mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
969 lines
52 KiB
Swift
969 lines
52 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import TelegramCore
|
|
import Postbox
|
|
import TextFormat
|
|
import UrlEscaping
|
|
|
|
struct PercentCounterItem: Comparable {
|
|
var index: Int = 0
|
|
var percent: Int = 0
|
|
var remainder: Int = 0
|
|
|
|
static func <(lhs: PercentCounterItem, rhs: PercentCounterItem) -> Bool {
|
|
if lhs.remainder > rhs.remainder {
|
|
return true
|
|
} else if lhs.remainder < rhs.remainder {
|
|
return false
|
|
}
|
|
return lhs.percent < rhs.percent
|
|
}
|
|
|
|
}
|
|
|
|
func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [PercentCounterItem] {
|
|
var left = left
|
|
var items = items.sorted(by: <)
|
|
var i:Int = 0
|
|
while i != items.count {
|
|
let item = items[i]
|
|
var j = i + 1
|
|
loop: while j != items.count {
|
|
if items[j].percent != item.percent || items[j].remainder != item.remainder {
|
|
break loop
|
|
}
|
|
j += 1
|
|
}
|
|
if items[i].remainder == 0 {
|
|
break
|
|
}
|
|
let equal = j - i
|
|
if equal <= left {
|
|
left -= equal
|
|
while i != j {
|
|
items[i].percent += 1
|
|
i += 1
|
|
}
|
|
} else {
|
|
i = j
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
func countNicePercent(votes: [Int], total: Int) -> [Int] {
|
|
var result:[Int] = []
|
|
var items:[PercentCounterItem] = []
|
|
for _ in votes {
|
|
result.append(0)
|
|
items.append(PercentCounterItem())
|
|
}
|
|
|
|
let count = votes.count
|
|
|
|
var left:Int = 100
|
|
for i in 0 ..< votes.count {
|
|
let votes = votes[i]
|
|
items[i].index = i
|
|
items[i].percent = Int((Float(votes) * 100) / Float(total))
|
|
items[i].remainder = (votes * 100) - (items[i].percent * total)
|
|
left -= items[i].percent
|
|
}
|
|
|
|
if left > 0 && left <= count {
|
|
items = adjustPercentCount(items, left: left)
|
|
}
|
|
for item in items {
|
|
result[item.index] = item.percent
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
private final class ChatMessagePollOptionRadioNodeParameters: NSObject {
|
|
let staticColor: UIColor
|
|
let animatedColor: UIColor
|
|
let offset: Double?
|
|
|
|
init(staticColor: UIColor, animatedColor: UIColor, offset: Double?) {
|
|
self.staticColor = staticColor
|
|
self.animatedColor = animatedColor
|
|
self.offset = offset
|
|
|
|
super.init()
|
|
}
|
|
}
|
|
|
|
private final class ChatMessagePollOptionRadioNode: ASDisplayNode {
|
|
private(set) var staticColor: UIColor?
|
|
private(set) var animatedColor: UIColor?
|
|
private var isInHierarchyValue: Bool = false
|
|
private(set) var isAnimating: Bool = false
|
|
private var startTime: Double?
|
|
|
|
private var displayLink: CADisplayLink?
|
|
|
|
private var shouldBeAnimating: Bool {
|
|
return self.isInHierarchyValue && self.isAnimating
|
|
}
|
|
|
|
override init() {
|
|
super.init()
|
|
|
|
self.isLayerBacked = true
|
|
self.isOpaque = false
|
|
}
|
|
|
|
override func willEnterHierarchy() {
|
|
super.willEnterHierarchy()
|
|
|
|
let previous = self.shouldBeAnimating
|
|
self.isInHierarchyValue = true
|
|
let updated = self.shouldBeAnimating
|
|
if previous != updated {
|
|
self.updateAnimating()
|
|
}
|
|
}
|
|
|
|
override func didExitHierarchy() {
|
|
super.didExitHierarchy()
|
|
|
|
let previous = self.shouldBeAnimating
|
|
self.isInHierarchyValue = false
|
|
let updated = self.shouldBeAnimating
|
|
if previous != updated {
|
|
self.updateAnimating()
|
|
}
|
|
}
|
|
|
|
func update(staticColor: UIColor, animatedColor: UIColor, isAnimating: Bool) {
|
|
var updated = false
|
|
if !staticColor.isEqual(self.staticColor) {
|
|
self.staticColor = staticColor
|
|
updated = true
|
|
}
|
|
if !animatedColor.isEqual(self.animatedColor) {
|
|
self.animatedColor = animatedColor
|
|
updated = true
|
|
}
|
|
if isAnimating != self.isAnimating {
|
|
let previous = self.shouldBeAnimating
|
|
self.isAnimating = isAnimating
|
|
let updated = self.shouldBeAnimating
|
|
if previous != updated {
|
|
self.updateAnimating()
|
|
}
|
|
}
|
|
if updated {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
private func updateAnimating() {
|
|
if self.shouldBeAnimating {
|
|
self.startTime = CACurrentMediaTime()
|
|
if self.displayLink == nil {
|
|
class DisplayLinkProxy: NSObject {
|
|
var f: () -> Void
|
|
init(_ f: @escaping () -> Void) {
|
|
self.f = f
|
|
}
|
|
|
|
@objc func displayLinkEvent() {
|
|
self.f()
|
|
}
|
|
}
|
|
let displayLink = CADisplayLink(target: DisplayLinkProxy({ [weak self] in
|
|
self?.setNeedsDisplay()
|
|
}), selector: #selector(DisplayLinkProxy.displayLinkEvent))
|
|
displayLink.add(to: .main, forMode: .common)
|
|
self.displayLink = displayLink
|
|
}
|
|
self.setNeedsDisplay()
|
|
} else if let displayLink = self.displayLink {
|
|
self.startTime = nil
|
|
displayLink.invalidate()
|
|
self.displayLink = nil
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
|
if let staticColor = self.staticColor, let animatedColor = self.animatedColor {
|
|
var offset: Double?
|
|
if let startTime = self.startTime {
|
|
offset = CACurrentMediaTime() - startTime
|
|
}
|
|
return ChatMessagePollOptionRadioNodeParameters(staticColor: staticColor, animatedColor: animatedColor, offset: offset)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
|
if isCancelled() {
|
|
return
|
|
}
|
|
|
|
guard let parameters = parameters as? ChatMessagePollOptionRadioNodeParameters else {
|
|
return
|
|
}
|
|
|
|
let context = UIGraphicsGetCurrentContext()!
|
|
|
|
if let offset = parameters.offset {
|
|
let t = max(0.0, offset)
|
|
let colorFadeInDuration = 0.2
|
|
let color: UIColor
|
|
if t < colorFadeInDuration {
|
|
color = parameters.staticColor.mixedWith(parameters.animatedColor, alpha: CGFloat(t / colorFadeInDuration))
|
|
} else {
|
|
color = parameters.animatedColor
|
|
}
|
|
context.setStrokeColor(color.cgColor)
|
|
|
|
let rotationDuration = 1.15
|
|
let rotationProgress = CGFloat(offset.truncatingRemainder(dividingBy: rotationDuration) / rotationDuration)
|
|
context.translateBy(x: bounds.midX, y: bounds.midY)
|
|
context.rotate(by: rotationProgress * 2.0 * CGFloat.pi)
|
|
context.translateBy(x: -bounds.midX, y: -bounds.midY)
|
|
|
|
let fillDuration = 1.0
|
|
if offset < fillDuration {
|
|
let fillT = CGFloat(offset.truncatingRemainder(dividingBy: fillDuration) / fillDuration)
|
|
let startAngle = fillT * 2.0 * CGFloat.pi - CGFloat.pi / 2.0
|
|
let endAngle = -CGFloat.pi / 2.0
|
|
|
|
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: (bounds.size.width - 1.0) / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
|
|
path.lineWidth = 1.0
|
|
path.lineCapStyle = .round
|
|
path.stroke()
|
|
} else {
|
|
let halfProgress: CGFloat = 0.7
|
|
let fillPhase = 0.6
|
|
let keepPhase = 0.0
|
|
let finishPhase = 0.6
|
|
let totalDuration = fillPhase + keepPhase + finishPhase
|
|
let localOffset = (offset - fillDuration).truncatingRemainder(dividingBy: totalDuration)
|
|
|
|
let angleOffsetT: CGFloat = -CGFloat(floor((offset - fillDuration) / totalDuration))
|
|
let angleOffset = (angleOffsetT * (1.0 - halfProgress) * 2.0 * CGFloat.pi).truncatingRemainder(dividingBy: 2.0 * CGFloat.pi)
|
|
context.translateBy(x: bounds.midX, y: bounds.midY)
|
|
context.rotate(by: angleOffset)
|
|
context.translateBy(x: -bounds.midX, y: -bounds.midY)
|
|
|
|
if localOffset < fillPhase + keepPhase {
|
|
let fillT = CGFloat(min(1.0, localOffset / fillPhase))
|
|
let startAngle = -CGFloat.pi / 2.0
|
|
let endAngle = (fillT * halfProgress) * 2.0 * CGFloat.pi - CGFloat.pi / 2.0
|
|
|
|
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: (bounds.size.width - 1.0) / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
|
|
path.lineWidth = 1.0
|
|
path.lineCapStyle = .round
|
|
path.stroke()
|
|
} else {
|
|
let finishT = CGFloat((localOffset - (fillPhase + keepPhase)) / finishPhase)
|
|
let endAngle = halfProgress * 2.0 * CGFloat.pi - CGFloat.pi / 2.0
|
|
let startAngle = -CGFloat.pi / 2.0 * (1.0 - finishT) + endAngle * finishT
|
|
|
|
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: (bounds.size.width - 1.0) / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
|
|
path.lineWidth = 1.0
|
|
path.lineCapStyle = .round
|
|
path.stroke()
|
|
}
|
|
}
|
|
} else {
|
|
context.setStrokeColor(parameters.staticColor.cgColor)
|
|
context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: bounds.width - 1.0, height: bounds.height - 1.0)))
|
|
}
|
|
}
|
|
}
|
|
|
|
private let percentageFont = Font.bold(14.5)
|
|
|
|
private func generatePercentageImage(presentationData: ChatPresentationData, incoming: Bool, value: Int) -> UIImage {
|
|
return generateImage(CGSize(width: 42.0, height: 20.0), rotatedContext: { size, context in
|
|
UIGraphicsPushContext(context)
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
let string = NSAttributedString(string: "\(value)%", font: percentageFont, textColor: incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor, paragraphAlignment: .right)
|
|
string.draw(in: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: size))
|
|
UIGraphicsPopContext()
|
|
})!
|
|
}
|
|
|
|
private func generatePercentageAnimationImages(presentationData: ChatPresentationData, incoming: Bool, from fromValue: Int, to toValue: Int, duration: Double) -> [UIImage] {
|
|
let minimumFrameDuration = 1.0 / 40.0
|
|
let numberOfFrames = max(1, Int(duration / minimumFrameDuration))
|
|
var images: [UIImage] = []
|
|
for i in 0 ..< numberOfFrames {
|
|
let t = CGFloat(i) / CGFloat(numberOfFrames)
|
|
images.append(generatePercentageImage(presentationData: presentationData, incoming: incoming, value: Int((1.0 - t) * CGFloat(fromValue) + t * CGFloat(toValue))))
|
|
}
|
|
return images
|
|
}
|
|
|
|
private struct ChatMessagePollOptionResult: Equatable {
|
|
let normalized: CGFloat
|
|
let percent: Int
|
|
let count: Int32
|
|
}
|
|
|
|
private final class ChatMessagePollOptionNode: ASDisplayNode {
|
|
private let highlightedBackgroundNode: ASImageNode
|
|
private var radioNode: ChatMessagePollOptionRadioNode?
|
|
private let percentageNode: ASDisplayNode
|
|
private var percentageImage: UIImage?
|
|
private var titleNode: TextNode?
|
|
private let buttonNode: HighlightTrackingButtonNode
|
|
private let separatorNode: ASDisplayNode
|
|
private let resultBarNode: ASImageNode
|
|
|
|
var option: TelegramMediaPollOption?
|
|
public private(set) var currentResult: ChatMessagePollOptionResult?
|
|
var pressed: (() -> Void)?
|
|
|
|
override init() {
|
|
self.highlightedBackgroundNode = ASImageNode()
|
|
self.highlightedBackgroundNode.displayWithoutProcessing = true
|
|
self.highlightedBackgroundNode.displaysAsynchronously = false
|
|
self.highlightedBackgroundNode.isLayerBacked = true
|
|
self.highlightedBackgroundNode.alpha = 0.0
|
|
self.highlightedBackgroundNode.isUserInteractionEnabled = false
|
|
|
|
self.buttonNode = HighlightTrackingButtonNode()
|
|
self.separatorNode = ASDisplayNode()
|
|
self.separatorNode.isLayerBacked = true
|
|
|
|
self.resultBarNode = ASImageNode()
|
|
self.resultBarNode.isLayerBacked = true
|
|
self.resultBarNode.alpha = 0.0
|
|
|
|
self.percentageNode = ASDisplayNode()
|
|
self.percentageNode.alpha = 0.0
|
|
self.percentageNode.isLayerBacked = true
|
|
//self.percentageNode.displaysAsynchronously = false
|
|
//self.percentageNode.displayWithoutProcessing = true
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.highlightedBackgroundNode)
|
|
self.addSubnode(self.separatorNode)
|
|
self.addSubnode(self.resultBarNode)
|
|
self.addSubnode(self.percentageNode)
|
|
self.addSubnode(self.buttonNode)
|
|
|
|
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
|
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.highlightedBackgroundNode.alpha = 1.0
|
|
} else {
|
|
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
|
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func buttonPressed() {
|
|
self.pressed?()
|
|
}
|
|
|
|
static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode))) {
|
|
let makeTitleLayout = TextNode.asyncLayout(maybeNode?.titleNode)
|
|
let currentResult = maybeNode?.currentResult
|
|
|
|
return { accountPeerId, presentationData, message, option, optionResult, constrainedWidth in
|
|
let leftInset: CGFloat = 50.0
|
|
let rightInset: CGFloat = 12.0
|
|
|
|
let incoming = message.effectivelyIncoming(accountPeerId)
|
|
|
|
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: option.text, font: presentationData.messageFont, textColor: incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - leftInset - rightInset), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)))
|
|
|
|
let contentHeight: CGFloat = max(46.0, titleLayout.size.height + 22.0)
|
|
|
|
let shouldHaveRadioNode = optionResult == nil
|
|
|
|
var updatedPercentageImage: UIImage?
|
|
if currentResult != optionResult {
|
|
updatedPercentageImage = generatePercentageImage(presentationData: presentationData, incoming: incoming, value: optionResult?.percent ?? 0)
|
|
}
|
|
|
|
return (titleLayout.size.width + leftInset + rightInset, { width in
|
|
return (CGSize(width: width, height: contentHeight), { animated, inProgress in
|
|
let node: ChatMessagePollOptionNode
|
|
if let maybeNode = maybeNode {
|
|
node = maybeNode
|
|
} else {
|
|
node = ChatMessagePollOptionNode()
|
|
}
|
|
|
|
node.option = option
|
|
let previousResult = node.currentResult
|
|
node.currentResult = optionResult
|
|
|
|
node.highlightedBackgroundNode.backgroundColor = (incoming ? presentationData.theme.theme.chat.message.incoming.accentTextColor : presentationData.theme.theme.chat.message.outgoing.accentTextColor).withAlphaComponent(0.15)
|
|
|
|
node.buttonNode.accessibilityLabel = option.text
|
|
|
|
let titleNode = titleApply()
|
|
if node.titleNode !== titleNode {
|
|
node.titleNode = titleNode
|
|
node.addSubnode(titleNode)
|
|
titleNode.isUserInteractionEnabled = false
|
|
}
|
|
if titleLayout.hasRTL {
|
|
titleNode.frame = CGRect(origin: CGPoint(x: width - rightInset - titleLayout.size.width, y: 11.0), size: titleLayout.size)
|
|
} else {
|
|
titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
|
|
}
|
|
|
|
if shouldHaveRadioNode {
|
|
let radioNode: ChatMessagePollOptionRadioNode
|
|
if let current = node.radioNode {
|
|
radioNode = current
|
|
} else {
|
|
radioNode = ChatMessagePollOptionRadioNode()
|
|
node.addSubnode(radioNode)
|
|
node.radioNode = radioNode
|
|
if animated {
|
|
radioNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
}
|
|
}
|
|
let radioSize: CGFloat = 22.0
|
|
radioNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: CGSize(width: radioSize, height: radioSize))
|
|
radioNode.update(staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, isAnimating: inProgress)
|
|
} else if let radioNode = node.radioNode {
|
|
node.radioNode = nil
|
|
if animated {
|
|
radioNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak radioNode] _ in
|
|
radioNode?.removeFromSupernode()
|
|
})
|
|
} else {
|
|
radioNode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
if let updatedPercentageImage = updatedPercentageImage {
|
|
node.percentageNode.contents = updatedPercentageImage.cgImage
|
|
node.percentageImage = updatedPercentageImage
|
|
}
|
|
if let image = node.percentageImage {
|
|
node.percentageNode.frame = CGRect(origin: CGPoint(x: leftInset - 7.0 - image.size.width, y: 12.0), size: image.size)
|
|
if animated && previousResult?.percent != optionResult?.percent {
|
|
let percentageDuration = 0.27
|
|
let images = generatePercentageAnimationImages(presentationData: presentationData, incoming: incoming, from: previousResult?.percent ?? 0, to: optionResult?.percent ?? 0, duration: percentageDuration)
|
|
if !images.isEmpty {
|
|
let animation = CAKeyframeAnimation(keyPath: "contents")
|
|
animation.values = images.map { $0.cgImage! }
|
|
animation.duration = percentageDuration * UIView.animationDurationFactor()
|
|
animation.calculationMode = .discrete
|
|
node.percentageNode.layer.add(animation, forKey: "image")
|
|
}
|
|
}
|
|
}
|
|
|
|
node.buttonNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: CGSize(width: width - 2.0, height: contentHeight))
|
|
node.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: contentHeight + UIScreenPixel))
|
|
|
|
node.separatorNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.separator : presentationData.theme.theme.chat.message.outgoing.polls.separator
|
|
node.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel))
|
|
|
|
if node.resultBarNode.image == nil {
|
|
node.resultBarNode.image = generateStretchableFilledCircleImage(diameter: 6.0, color: incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar)
|
|
}
|
|
|
|
let minBarWidth: CGFloat = 6.0
|
|
let resultBarWidth = minBarWidth + floor((width - leftInset - rightInset - minBarWidth) * (optionResult?.normalized ?? 0.0))
|
|
node.resultBarNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: resultBarWidth, height: 6.0))
|
|
node.resultBarNode.alpha = optionResult != nil ? 1.0 : 0.0
|
|
node.percentageNode.alpha = optionResult != nil ? 1.0 : 0.0
|
|
node.separatorNode.alpha = optionResult == nil ? 1.0 : 0.0
|
|
if animated, currentResult != optionResult {
|
|
if (currentResult != nil) != (optionResult != nil) {
|
|
if optionResult != nil {
|
|
node.resultBarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
|
node.percentageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
node.separatorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08)
|
|
} else {
|
|
node.resultBarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4)
|
|
node.percentageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
|
node.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
node.buttonNode.isAccessibilityElement = shouldHaveRadioNode
|
|
|
|
let previousResultBarWidth = minBarWidth + floor((width - leftInset - rightInset - minBarWidth) * (currentResult?.normalized ?? 0.0))
|
|
let previousFrame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: previousResultBarWidth, height: 6.0))
|
|
|
|
node.resultBarNode.layer.animateSpring(from: NSValue(cgPoint: previousFrame.center), to: NSValue(cgPoint: node.resultBarNode.frame.center), keyPath: "position", duration: 0.6, damping: 110.0)
|
|
node.resultBarNode.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: previousFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: node.resultBarNode.frame.size)), keyPath: "bounds", duration: 0.6, damping: 110.0)
|
|
}
|
|
|
|
return node
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private let labelsFont = Font.regular(14.0)
|
|
|
|
class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
|
private let textNode: TextNode
|
|
private let typeNode: TextNode
|
|
private let votersNode: TextNode
|
|
private let statusNode: ChatMessageDateAndStatusNode
|
|
private var optionNodes: [ChatMessagePollOptionNode] = []
|
|
|
|
required init() {
|
|
self.textNode = TextNode()
|
|
self.textNode.isUserInteractionEnabled = false
|
|
self.textNode.contentMode = .topLeft
|
|
self.textNode.contentsScale = UIScreenScale
|
|
self.textNode.displaysAsynchronously = true
|
|
|
|
self.typeNode = TextNode()
|
|
self.typeNode.isUserInteractionEnabled = false
|
|
self.typeNode.contentMode = .topLeft
|
|
self.typeNode.contentsScale = UIScreenScale
|
|
self.typeNode.displaysAsynchronously = true
|
|
|
|
self.votersNode = TextNode()
|
|
self.votersNode.isUserInteractionEnabled = false
|
|
self.votersNode.contentMode = .topLeft
|
|
self.votersNode.contentsScale = UIScreenScale
|
|
self.votersNode.displaysAsynchronously = true
|
|
|
|
self.statusNode = ChatMessageDateAndStatusNode()
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.textNode)
|
|
self.addSubnode(self.typeNode)
|
|
self.addSubnode(self.votersNode)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
|
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
|
let makeTypeLayout = TextNode.asyncLayout(self.typeNode)
|
|
let makeVotersLayout = TextNode.asyncLayout(self.votersNode)
|
|
let statusLayout = self.statusNode.asyncLayout()
|
|
|
|
var previousPoll: TelegramMediaPoll?
|
|
if let item = self.item {
|
|
for media in item.message.media {
|
|
if let media = media as? TelegramMediaPoll {
|
|
previousPoll = media
|
|
}
|
|
}
|
|
}
|
|
|
|
var previousOptionNodeLayouts: [Data: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))] = [:]
|
|
for optionNode in self.optionNodes {
|
|
if let option = optionNode.option {
|
|
previousOptionNodeLayouts[option.opaqueIdentifier] = ChatMessagePollOptionNode.asyncLayout(optionNode)
|
|
}
|
|
}
|
|
|
|
return { item, layoutConstants, _, _, _ in
|
|
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
|
|
|
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
|
let message = item.message
|
|
|
|
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
|
|
|
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
|
let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height)
|
|
|
|
var edited = false
|
|
var viewCount: Int?
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? EditedMessageAttribute {
|
|
edited = !attribute.isHidden
|
|
} else if let attribute = attribute as? ViewCountMessageAttribute {
|
|
viewCount = attribute.count
|
|
}
|
|
}
|
|
|
|
var dateReactions: [MessageReaction] = []
|
|
var dateReactionCount = 0
|
|
if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty {
|
|
for reaction in reactionsAttribute.reactions {
|
|
if reaction.isSelected {
|
|
dateReactions.insert(reaction, at: 0)
|
|
} else {
|
|
dateReactions.append(reaction)
|
|
}
|
|
dateReactionCount += Int(reaction.count)
|
|
}
|
|
}
|
|
|
|
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount)
|
|
|
|
let statusType: ChatMessageDateAndStatusType?
|
|
switch position {
|
|
case .linear(_, .None):
|
|
if incoming {
|
|
statusType = .BubbleIncoming
|
|
} else {
|
|
if message.flags.contains(.Failed) {
|
|
statusType = .BubbleOutgoing(.Failed)
|
|
} else if message.flags.isSending && !message.isSentOrAcknowledged {
|
|
statusType = .BubbleOutgoing(.Sending)
|
|
} else {
|
|
statusType = .BubbleOutgoing(.Sent(read: item.read))
|
|
}
|
|
}
|
|
default:
|
|
statusType = nil
|
|
}
|
|
|
|
var statusSize: CGSize?
|
|
var statusApply: ((Bool) -> Void)?
|
|
|
|
if let statusType = statusType {
|
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions)
|
|
statusSize = size
|
|
statusApply = apply
|
|
}
|
|
|
|
var poll: TelegramMediaPoll?
|
|
for media in item.message.media {
|
|
if let media = media as? TelegramMediaPoll {
|
|
poll = media
|
|
break
|
|
}
|
|
}
|
|
|
|
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
|
|
|
|
let attributedText = NSAttributedString(string: poll?.text ?? "", font: item.presentationData.messageBoldFont, textColor: messageTheme.primaryTextColor)
|
|
|
|
let textInsets = UIEdgeInsets(top: 2.0, left: 0.0, bottom: 5.0, right: 0.0)
|
|
|
|
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets))
|
|
let typeText: String
|
|
if let poll = poll, poll.isClosed {
|
|
typeText = item.presentationData.strings.MessagePoll_LabelClosed
|
|
} else {
|
|
typeText = item.presentationData.strings.MessagePoll_LabelAnonymous
|
|
}
|
|
let (typeLayout, typeApply) = makeTypeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: typeText, font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let votersString: String
|
|
if let totalVoters = poll?.results.totalVoters {
|
|
if totalVoters == 0 {
|
|
votersString = item.presentationData.strings.MessagePoll_NoVotes
|
|
} else {
|
|
votersString = item.presentationData.strings.MessagePoll_VotedCount(totalVoters)
|
|
}
|
|
} else {
|
|
votersString = " "
|
|
}
|
|
let (votersLayout, votersApply) = makeVotersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: votersString, font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets))
|
|
|
|
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size)
|
|
var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom))
|
|
|
|
var statusFrame: CGRect?
|
|
if let statusSize = statusSize {
|
|
statusFrame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.maxX - statusSize.width, y: textFrameWithoutInsets.maxY - statusSize.height), size: statusSize)
|
|
}
|
|
|
|
textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
|
|
textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
|
|
statusFrame = statusFrame?.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
|
|
|
|
var boundingSize: CGSize = textFrameWithoutInsets.size
|
|
boundingSize.width = max(boundingSize.width, typeLayout.size.width)
|
|
boundingSize.width = max(boundingSize.width, votersLayout.size.width + 4.0 + (statusSize?.width ?? 0.0))
|
|
|
|
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
|
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
|
|
|
|
var pollOptionsFinalizeLayouts: [(CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)] = []
|
|
if let poll = poll {
|
|
var optionVoterCount: [Int: Int32] = [:]
|
|
var maxOptionVoterCount: Int32 = 0
|
|
var totalVoterCount: Int32 = 0
|
|
let voters: [TelegramMediaPollOptionVoters]?
|
|
if poll.isClosed {
|
|
voters = poll.results.voters ?? []
|
|
} else {
|
|
voters = poll.results.voters
|
|
}
|
|
if let voters = voters, let totalVoters = poll.results.totalVoters {
|
|
var didVote = false
|
|
for voter in voters {
|
|
if voter.selected {
|
|
didVote = true
|
|
}
|
|
}
|
|
totalVoterCount = totalVoters
|
|
if didVote || poll.isClosed {
|
|
for i in 0 ..< poll.options.count {
|
|
inner: for optionVoters in voters {
|
|
if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier {
|
|
optionVoterCount[i] = optionVoters.count
|
|
maxOptionVoterCount = max(maxOptionVoterCount, optionVoters.count)
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var optionVoterCounts: [Int]
|
|
if totalVoterCount != 0 {
|
|
optionVoterCounts = countNicePercent(votes: (0 ..< poll.options.count).map({ Int(optionVoterCount[$0] ?? 0) }), total: Int(totalVoterCount))
|
|
} else {
|
|
optionVoterCounts = Array(repeating: 0, count: poll.options.count)
|
|
}
|
|
|
|
for i in 0 ..< poll.options.count {
|
|
let option = poll.options[i]
|
|
|
|
let makeLayout: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))
|
|
if let previous = previousOptionNodeLayouts[option.opaqueIdentifier] {
|
|
makeLayout = previous
|
|
} else {
|
|
makeLayout = ChatMessagePollOptionNode.asyncLayout(nil)
|
|
}
|
|
var optionResult: ChatMessagePollOptionResult?
|
|
if let count = optionVoterCount[i] {
|
|
if maxOptionVoterCount != 0 && totalVoterCount != 0 {
|
|
optionResult = ChatMessagePollOptionResult(normalized: CGFloat(count) / CGFloat(maxOptionVoterCount), percent: optionVoterCounts[i], count: count)
|
|
} else if poll.isClosed {
|
|
optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0)
|
|
}
|
|
} else if poll.isClosed {
|
|
optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0)
|
|
}
|
|
let result = makeLayout(item.context.account.peerId, item.presentationData, item.message, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
|
|
boundingSize.width = max(boundingSize.width, result.minimumWidth + layoutConstants.bubble.borderInset * 2.0)
|
|
pollOptionsFinalizeLayouts.append(result.1)
|
|
}
|
|
}
|
|
|
|
boundingSize.width = max(boundingSize.width, min(270.0, constrainedSize.width))
|
|
|
|
var canVote = false
|
|
if item.message.id.namespace == Namespaces.Message.Cloud, let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !poll.isClosed {
|
|
var hasVoted = false
|
|
if let voters = poll.results.voters {
|
|
for voter in voters {
|
|
if voter.selected {
|
|
hasVoted = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !hasVoted {
|
|
canVote = true
|
|
}
|
|
}
|
|
if Namespaces.Message.allScheduled.contains(item.message.id.namespace) {
|
|
canVote = true
|
|
}
|
|
|
|
return (boundingSize.width, { boundingWidth in
|
|
var resultSize = CGSize(width: max(boundingSize.width, boundingWidth), height: boundingSize.height)
|
|
|
|
let titleTypeSpacing: CGFloat = -4.0
|
|
let typeOptionsSpacing: CGFloat = 3.0
|
|
resultSize.height += titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing
|
|
|
|
var optionNodesSizesAndApply: [(CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)] = []
|
|
for finalizeLayout in pollOptionsFinalizeLayouts {
|
|
let result = finalizeLayout(boundingWidth - layoutConstants.bubble.borderInset * 2.0)
|
|
resultSize.width = max(resultSize.width, result.0.width + layoutConstants.bubble.borderInset * 2.0)
|
|
resultSize.height += result.0.height
|
|
optionNodesSizesAndApply.append(result)
|
|
}
|
|
|
|
let optionsVotersSpacing: CGFloat = 11.0
|
|
let votersBottomSpacing: CGFloat = 8.0
|
|
resultSize.height += optionsVotersSpacing + votersLayout.size.height + votersBottomSpacing
|
|
|
|
var adjustedStatusFrame: CGRect?
|
|
if let statusFrame = statusFrame {
|
|
adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: resultSize.height - statusFrame.size.height - 6.0), size: statusFrame.size)
|
|
}
|
|
|
|
return (resultSize, { [weak self] animation, _ in
|
|
if let strongSelf = self {
|
|
strongSelf.item = item
|
|
|
|
let cachedLayout = strongSelf.textNode.cachedLayout
|
|
|
|
if case .System = animation {
|
|
if let cachedLayout = cachedLayout {
|
|
if cachedLayout != textLayout {
|
|
if let textContents = strongSelf.textNode.contents {
|
|
let fadeNode = ASDisplayNode()
|
|
fadeNode.displaysAsynchronously = false
|
|
fadeNode.contents = textContents
|
|
fadeNode.frame = strongSelf.textNode.frame
|
|
fadeNode.isLayerBacked = true
|
|
strongSelf.addSubnode(fadeNode)
|
|
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
|
|
fadeNode?.removeFromSupernode()
|
|
})
|
|
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let _ = textApply()
|
|
let _ = typeApply()
|
|
|
|
var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing
|
|
var updatedOptionNodes: [ChatMessagePollOptionNode] = []
|
|
for i in 0 ..< optionNodesSizesAndApply.count {
|
|
let (size, apply) = optionNodesSizesAndApply[i]
|
|
var isRequesting = false
|
|
if let poll = poll, i < poll.options.count {
|
|
isRequesting = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == poll.options[i].opaqueIdentifier
|
|
}
|
|
let optionNode = apply(animation.isAnimated, isRequesting)
|
|
if optionNode.supernode !== strongSelf {
|
|
strongSelf.addSubnode(optionNode)
|
|
let option = optionNode.option
|
|
optionNode.pressed = {
|
|
guard let strongSelf = self, let item = strongSelf.item, let option = option else {
|
|
return
|
|
}
|
|
|
|
item.controllerInteraction.requestSelectMessagePollOption(item.message.id, option.opaqueIdentifier)
|
|
}
|
|
}
|
|
optionNode.frame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
|
|
verticalOffset += size.height
|
|
updatedOptionNodes.append(optionNode)
|
|
optionNode.isUserInteractionEnabled = canVote && item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == nil
|
|
}
|
|
for optionNode in strongSelf.optionNodes {
|
|
if !updatedOptionNodes.contains(where: { $0 === optionNode }) {
|
|
optionNode.removeFromSupernode()
|
|
}
|
|
}
|
|
strongSelf.optionNodes = updatedOptionNodes
|
|
|
|
if let statusApply = statusApply, let adjustedStatusFrame = adjustedStatusFrame {
|
|
let previousStatusFrame = strongSelf.statusNode.frame
|
|
strongSelf.statusNode.frame = adjustedStatusFrame
|
|
var hasAnimation = true
|
|
if case .None = animation {
|
|
hasAnimation = false
|
|
}
|
|
statusApply(hasAnimation)
|
|
if strongSelf.statusNode.supernode == nil {
|
|
strongSelf.addSubnode(strongSelf.statusNode)
|
|
} else {
|
|
if case let .System(duration) = animation {
|
|
let delta = CGPoint(x: previousStatusFrame.maxX - adjustedStatusFrame.maxX, y: previousStatusFrame.minY - adjustedStatusFrame.minY)
|
|
let statusPosition = strongSelf.statusNode.layer.position
|
|
let previousPosition = CGPoint(x: statusPosition.x + delta.x, y: statusPosition.y + delta.y)
|
|
strongSelf.statusNode.layer.animatePosition(from: previousPosition, to: statusPosition, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
}
|
|
} else if strongSelf.statusNode.supernode != nil {
|
|
strongSelf.statusNode.removeFromSupernode()
|
|
}
|
|
|
|
if textLayout.hasRTL {
|
|
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right, y: textFrame.origin.y), size: textFrame.size)
|
|
} else {
|
|
strongSelf.textNode.frame = textFrame
|
|
}
|
|
strongSelf.typeNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size)
|
|
|
|
let _ = votersApply()
|
|
strongSelf.votersNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: verticalOffset + optionsVotersSpacing), size: votersLayout.size)
|
|
if animation.isAnimated, let previousPoll = previousPoll, let poll = poll {
|
|
if previousPoll.results.totalVoters == nil && poll.results.totalVoters != nil {
|
|
strongSelf.votersNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
|
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
|
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
|
self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
|
|
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
|
|
let textNodeFrame = self.textNode.frame
|
|
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
|
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
|
var concealed = true
|
|
if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
|
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
|
}
|
|
return .url(url: url, concealed: concealed)
|
|
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
|
return .peerMention(peerMention.peerId, peerMention.mention)
|
|
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
|
return .textMention(peerName)
|
|
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
|
|
return .botCommand(botCommand)
|
|
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
|
return .hashtag(hashtag.peerName, hashtag.hashtag)
|
|
} else {
|
|
return .none
|
|
}
|
|
} else {
|
|
for optionNode in self.optionNodes {
|
|
if optionNode.frame.contains(point) {
|
|
if optionNode.isUserInteractionEnabled {
|
|
return .ignore
|
|
} else if let result = optionNode.currentResult, let item = self.item {
|
|
let string: String
|
|
if result.count == 0 {
|
|
string = item.presentationData.strings.MessagePoll_NoVotes
|
|
} else {
|
|
string = item.presentationData.strings.MessagePoll_VotedCount(result.count)
|
|
}
|
|
return .tooltip(string, optionNode, optionNode.bounds.offsetBy(dx: 0.0, dy: 10.0))
|
|
}
|
|
}
|
|
}
|
|
return .none
|
|
}
|
|
}
|
|
|
|
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
|
if !self.statusNode.isHidden {
|
|
return self.statusNode.reactionNode(value: value)
|
|
}
|
|
return nil
|
|
}
|
|
}
|