mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Initial reactions proof of concept
This commit is contained in:
parent
ae165ed4a5
commit
9bad9b86ab
@ -130,7 +130,7 @@ public extension CALayer {
|
||||
self.add(animationGroup, forKey: key)
|
||||
}
|
||||
|
||||
public func animateKeyframes(values: [AnyObject], duration: Double, keyPath: String, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
public func animateKeyframes(values: [AnyObject], duration: Double, keyPath: String, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
let k = Float(UIView.animationDurationFactor())
|
||||
var speed: Float = 1.0
|
||||
if k != 0 && k != 1 {
|
||||
@ -152,6 +152,7 @@ public extension CALayer {
|
||||
animation.keyTimes = keyTimes
|
||||
animation.speed = speed
|
||||
animation.duration = duration
|
||||
animation.isAdditive = additive
|
||||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat) -> UI
|
||||
}
|
||||
|
||||
private final class ReactionNode: ASDisplayNode {
|
||||
private let reaction: ReactionGestureItem
|
||||
let reaction: ReactionGestureItem
|
||||
private let animationNode: AnimatedStickerNode
|
||||
var isMaximized: Bool?
|
||||
private let intrinsicSize: CGSize
|
||||
@ -91,6 +91,9 @@ final class ReactionSelectionNode: ASDisplayNode {
|
||||
private let backgroundShadowNode: ASImageNode
|
||||
private let bubbleNodes: [(ASImageNode, ASImageNode)]
|
||||
private let reactionNodes: [ReactionNode]
|
||||
private var hasSelectedNode = false
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
public init(account: Account, reactions: [ReactionGestureItem]) {
|
||||
self.backgroundNode = ASImageNode()
|
||||
@ -142,7 +145,7 @@ final class ReactionSelectionNode: ASDisplayNode {
|
||||
let contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing
|
||||
|
||||
var backgroundFrame = CGRect(origin: CGPoint(x: -shadowBlur, y: -shadowBlur), size: CGSize(width: contentWidth + shadowBlur * 2.0, height: backgroundHeight + shadowBlur * 2.0))
|
||||
backgroundFrame = backgroundFrame.offsetBy(dx: startingPoint.x - contentWidth + backgroundHeight / 2.0, dy: startingPoint.y - backgroundHeight - 16.0)
|
||||
backgroundFrame = backgroundFrame.offsetBy(dx: startingPoint.x - contentWidth + backgroundHeight / 2.0 - 52.0, dy: startingPoint.y - backgroundHeight - 16.0)
|
||||
|
||||
self.backgroundNode.frame = backgroundFrame
|
||||
self.backgroundShadowNode.frame = backgroundFrame
|
||||
@ -152,8 +155,15 @@ final class ReactionSelectionNode: ASDisplayNode {
|
||||
let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart))
|
||||
|
||||
var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSpacing
|
||||
if offsetFromStart > backgroundFrame.maxX - shadowBlur {
|
||||
self.hasSelectedNode = false
|
||||
} else {
|
||||
self.hasSelectedNode = true
|
||||
}
|
||||
|
||||
var maximizedIndex = Int(((anchorX - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count))
|
||||
maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex))
|
||||
|
||||
for i in 0 ..< self.reactionNodes.count {
|
||||
let isMaximized = i == maximizedIndex
|
||||
|
||||
@ -174,6 +184,9 @@ final class ReactionSelectionNode: ASDisplayNode {
|
||||
if self.reactionNodes[i].isMaximized != isMaximized {
|
||||
self.reactionNodes[i].isMaximized = isMaximized
|
||||
self.reactionNodes[i].updateIsAnimating(isMaximized, animated: !isInitial)
|
||||
if isMaximized {
|
||||
self.hapticFeedback.tap()
|
||||
}
|
||||
}
|
||||
|
||||
var reactionFrame = CGRect(origin: CGPoint(x: reactionX, y: backgroundFrame.maxY - shadowBlur - minimizedReactionVerticalInset - reactionSize), size: CGSize(width: reactionSize, height: reactionSize))
|
||||
@ -209,8 +222,10 @@ final class ReactionSelectionNode: ASDisplayNode {
|
||||
|
||||
for i in 0 ..< self.reactionNodes.count {
|
||||
let animationOffset: Double = 1.0 - Double(i) / Double(self.reactionNodes.count - 1)
|
||||
let nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.maxX - shadowBlur) / 2.0 - 42.0, y: self.reactionNodes[i].frame.minY - self.backgroundNode.frame.maxY - shadowBlur)
|
||||
self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5 + animationOffset * 0.05, initialVelocity: 0.0, damping: damping)
|
||||
var nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.maxX - shadowBlur) / 2.0 - 42.0, y: self.reactionNodes[i].frame.minY - self.backgroundNode.frame.maxY - shadowBlur)
|
||||
nodeOffset.x = -nodeOffset.x
|
||||
nodeOffset.y = 30.0
|
||||
self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5 + animationOffset * 0.08, initialVelocity: 0.0, damping: damping)
|
||||
self.reactionNodes[i].layer.animateSpring(from: NSValue(cgPoint: nodeOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true)
|
||||
}
|
||||
|
||||
@ -220,7 +235,110 @@ final class ReactionSelectionNode: ASDisplayNode {
|
||||
self.backgroundShadowNode.layer.animateSpring(from: NSValue(cgPoint: backgroundOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true)
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
completion()
|
||||
func animateOut(into targetNode: ASImageNode?, hideTarget: Bool, completion: @escaping () -> Void) {
|
||||
self.hapticFeedback.prepareTap()
|
||||
|
||||
var completedContainer = false
|
||||
var completedTarget = true
|
||||
|
||||
let intermediateCompletion: () -> Void = {
|
||||
if completedContainer && completedTarget {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
if let targetNode = targetNode {
|
||||
for i in 0 ..< self.reactionNodes.count {
|
||||
if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized {
|
||||
if let snapshotView = self.reactionNodes[i].view.snapshotContentTree() {
|
||||
let targetSnapshotView = UIImageView()
|
||||
targetSnapshotView.image = targetNode.image
|
||||
targetSnapshotView.frame = self.view.convert(targetNode.bounds, from: targetNode.view)
|
||||
self.reactionNodes[i].isHidden = true
|
||||
self.view.addSubview(targetSnapshotView)
|
||||
self.view.addSubview(snapshotView)
|
||||
completedTarget = false
|
||||
let targetPosition = self.view.convert(targetNode.bounds.center, from: targetNode.view)
|
||||
let duration: Double = 0.3
|
||||
if hideTarget {
|
||||
targetNode.isHidden = true
|
||||
}
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||
targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
targetSnapshotView.layer.animateScale(from: snapshotView.bounds.width / targetSnapshotView.bounds.width, to: 0.5, duration: 0.3, removeOnCompletion: false)
|
||||
|
||||
|
||||
let sourcePoint = snapshotView.center
|
||||
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - 30.0)
|
||||
|
||||
let x1 = sourcePoint.x
|
||||
let y1 = sourcePoint.y
|
||||
let x2 = midPoint.x
|
||||
let y2 = midPoint.y
|
||||
let x3 = targetPosition.x
|
||||
let y3 = targetPosition.y
|
||||
|
||||
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||
|
||||
var keyframes: [AnyObject] = []
|
||||
for i in 0 ..< 10 {
|
||||
let k = CGFloat(i) / CGFloat(10 - 1)
|
||||
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
|
||||
let y = a * x * x + b * x + c
|
||||
keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y)))
|
||||
}
|
||||
|
||||
snapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.hapticFeedback.tap()
|
||||
}
|
||||
completedTarget = true
|
||||
if hideTarget {
|
||||
targetNode.isHidden = false
|
||||
targetNode.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0)
|
||||
}
|
||||
intermediateCompletion()
|
||||
})
|
||||
targetSnapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false)
|
||||
|
||||
snapshotView.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 0.5) / snapshotView.bounds.width, duration: 0.3, removeOnCompletion: false)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.backgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
self.backgroundShadowNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.backgroundShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
completedContainer = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
for (node, shadow) in self.bubbleNodes {
|
||||
node.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
shadow.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
shadow.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
for i in 0 ..< self.reactionNodes.count {
|
||||
self.reactionNodes[i].layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
self.reactionNodes[i].layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
func selectedReaction() -> ReactionGestureItem? {
|
||||
if !self.hasSelectedNode {
|
||||
return nil
|
||||
}
|
||||
for i in 0 ..< self.reactionNodes.count {
|
||||
if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized {
|
||||
return self.reactionNodes[i].reaction
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -36,9 +36,16 @@ public final class ReactionSelectionParentNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func dismissReactions() {
|
||||
func selectedReaction() -> ReactionGestureItem? {
|
||||
if let currentNode = self.currentNode {
|
||||
currentNode.animateOut(completion: { [weak currentNode] in
|
||||
return currentNode.selectedReaction()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dismissReactions(into targetNode: ASImageNode?, hideTarget: Bool) {
|
||||
if let currentNode = self.currentNode {
|
||||
currentNode.animateOut(into: targetNode, hideTarget: hideTarget, completion: { [weak currentNode] in
|
||||
currentNode?.removeFromSupernode()
|
||||
})
|
||||
self.currentNode = nil
|
||||
|
@ -8,12 +8,15 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
||||
private var firstLocation: CGPoint = CGPoint()
|
||||
private var currentReactions: [ReactionGestureItem] = []
|
||||
private var isActivated = false
|
||||
private var isAwaitingCompletion = false
|
||||
private weak var currentContainer: ReactionSelectionParentNode?
|
||||
|
||||
public var availableReactions: (() -> [ReactionGestureItem])?
|
||||
public var getReactionContainer: (() -> ReactionSelectionParentNode?)?
|
||||
public var updateOffset: ((CGFloat, Bool) -> Void)?
|
||||
public var completed: (() -> Void)?
|
||||
public var completed: ((ReactionGestureItem?) -> Void)?
|
||||
public var displayReply: ((CGFloat) -> Void)?
|
||||
public var activateReply: (() -> Void)?
|
||||
|
||||
override public init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
@ -27,6 +30,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
||||
self.validatedGesture = false
|
||||
self.currentReactions = []
|
||||
self.isActivated = false
|
||||
self.isAwaitingCompletion = false
|
||||
}
|
||||
|
||||
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
@ -42,6 +46,9 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
||||
}
|
||||
|
||||
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
if self.isAwaitingCompletion {
|
||||
return
|
||||
}
|
||||
guard let _ = self.view else {
|
||||
return
|
||||
}
|
||||
@ -49,7 +56,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
||||
return
|
||||
}
|
||||
|
||||
let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y)
|
||||
var translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y)
|
||||
|
||||
let absTranslationX: CGFloat = abs(translation.x)
|
||||
let absTranslationY: CGFloat = abs(translation.y)
|
||||
@ -63,7 +70,9 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
||||
self.state = .failed
|
||||
} else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX {
|
||||
self.validatedGesture = true
|
||||
self.updateOffset?(translation.x, true)
|
||||
self.firstLocation = location
|
||||
translation = CGPoint()
|
||||
self.updateOffset?(0.0, false)
|
||||
updatedOffset = true
|
||||
}
|
||||
}
|
||||
@ -75,6 +84,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
||||
if !self.isActivated {
|
||||
if absTranslationX > 40.0 {
|
||||
self.isActivated = true
|
||||
self.displayReply?(-min(0.0, translation.x))
|
||||
if !self.currentReactions.isEmpty, let reactionContainer = self.getReactionContainer?() {
|
||||
self.currentContainer = reactionContainer
|
||||
let reactionContainerLocation = reactionContainer.view.convert(location, from: nil)
|
||||
@ -92,12 +102,40 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
||||
}
|
||||
|
||||
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
if self.validatedGesture {
|
||||
self.completed?()
|
||||
if self.isAwaitingCompletion {
|
||||
return
|
||||
}
|
||||
guard let location = touches.first?.location(in: nil) else {
|
||||
return
|
||||
}
|
||||
if self.validatedGesture {
|
||||
let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y)
|
||||
if let reaction = self.currentContainer?.selectedReaction() {
|
||||
self.completed?(reaction)
|
||||
self.isAwaitingCompletion = true
|
||||
} else {
|
||||
if translation.x < -40.0 {
|
||||
self.currentContainer?.dismissReactions(into: nil, hideTarget: false)
|
||||
self.activateReply?()
|
||||
self.state = .ended
|
||||
} else {
|
||||
self.currentContainer?.dismissReactions(into: nil, hideTarget: false)
|
||||
self.completed?(nil)
|
||||
self.state = .cancelled
|
||||
super.touchesEnded(touches, with: event)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.currentContainer?.dismissReactions(into: nil, hideTarget: false)
|
||||
self.state = .cancelled
|
||||
super.touchesEnded(touches, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
public func complete(into targetNode: ASImageNode?, hideTarget: Bool) {
|
||||
if self.isAwaitingCompletion {
|
||||
self.currentContainer?.dismissReactions(into: targetNode, hideTarget: hideTarget)
|
||||
self.state = .ended
|
||||
}
|
||||
self.currentContainer?.dismissReactions()
|
||||
self.state = .ended
|
||||
|
||||
super.touchesEnded(touches, with: event)
|
||||
}
|
||||
}
|
||||
|
@ -1531,6 +1531,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
window.rootViewController?.present(controller, animated: true)
|
||||
}
|
||||
}
|
||||
}, updateMessageReaction: { [weak self] messageId, reaction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, reactions: [reaction]).start()
|
||||
}, requestMessageUpdate: { [weak self] id in
|
||||
if let strongSelf = self {
|
||||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
|
||||
|
@ -94,6 +94,7 @@ public final class ChatControllerInteraction {
|
||||
let sendScheduledMessagesNow: ([MessageId]) -> Void
|
||||
let editScheduledMessagesTime: ([MessageId]) -> Void
|
||||
let performTextSelectionAction: (UInt32, String, TextSelectionAction) -> Void
|
||||
let updateMessageReaction: (MessageId, String) -> Void
|
||||
|
||||
let requestMessageUpdate: (MessageId) -> Void
|
||||
let cancelInteractiveKeyboardGestures: () -> Void
|
||||
@ -108,7 +109,7 @@ public final class ChatControllerInteraction {
|
||||
var searchTextHighightState: String?
|
||||
var seenOneTimeAnimatedMedia = Set<MessageId>()
|
||||
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
self.openMessage = openMessage
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
@ -156,6 +157,7 @@ public final class ChatControllerInteraction {
|
||||
self.sendScheduledMessagesNow = sendScheduledMessagesNow
|
||||
self.editScheduledMessagesTime = editScheduledMessagesTime
|
||||
self.performTextSelectionAction = performTextSelectionAction
|
||||
self.updateMessageReaction = updateMessageReaction
|
||||
|
||||
self.requestMessageUpdate = requestMessageUpdate
|
||||
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
|
||||
@ -190,6 +192,7 @@ public final class ChatControllerInteraction {
|
||||
}, sendScheduledMessagesNow: { _ in
|
||||
}, editScheduledMessagesTime: { _ in
|
||||
}, performTextSelectionAction: { _, _, _ in
|
||||
}, updateMessageReaction: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
@ -462,9 +462,22 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .minimal)
|
||||
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 (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, false, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .minimal, reactionCount: dateReactionCount)
|
||||
|
||||
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, false, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions)
|
||||
|
||||
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
||||
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
|
||||
|
@ -314,7 +314,20 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings)
|
||||
var dateReactions: [MessageReaction] = []
|
||||
var dateReactionCount = 0
|
||||
if let reactionsAttribute = mergedMessageReactions(attributes: 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: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, reactionCount: dateReactionCount)
|
||||
|
||||
var webpageGalleryMediaCount: Int?
|
||||
for media in message.media {
|
||||
@ -534,7 +547,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize)
|
||||
statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -1023,4 +1036,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
return self.contentImageNode?.playMediaWithSound()
|
||||
}
|
||||
|
||||
func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
if !self.statusNode.isHidden {
|
||||
return self.statusNode.reactionNode(value: value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -172,4 +172,8 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
|
||||
|
||||
func updateIsExtractedToContextPreview(_ value: Bool) {
|
||||
}
|
||||
|
||||
func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -179,6 +179,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
private var appliedForwardInfo: (Peer?, String?)?
|
||||
|
||||
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||
private var reactionRecognizer: ReactionSwipeGestureRecognizer?
|
||||
|
||||
private var awaitingAppliedReaction: String?
|
||||
|
||||
override var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
@ -391,6 +394,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
self.view.addGestureRecognizer(replyRecognizer)*/
|
||||
|
||||
let reactionRecognizer = ReactionSwipeGestureRecognizer(target: nil, action: nil)
|
||||
self.reactionRecognizer = reactionRecognizer
|
||||
reactionRecognizer.availableReactions = { [weak self] in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return []
|
||||
@ -410,7 +414,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
if !item.controllerInteraction.canSetupReply(item.message) {
|
||||
return []
|
||||
//return []
|
||||
}
|
||||
|
||||
let reactions: [(String, String)] = [
|
||||
@ -442,9 +446,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
if animated {
|
||||
strongSelf.layer.animateBoundsOriginXAdditive(from: -offset, to: 0.0, duration: 0.1, mediaTimingFunction: CAMediaTimingFunction(name: .easeOut))
|
||||
}
|
||||
if let swipeToReplyNode = strongSelf.swipeToReplyNode {
|
||||
swipeToReplyNode.alpha = max(0.0, min(1.0, abs(offset / 40.0)))
|
||||
}
|
||||
}
|
||||
reactionRecognizer.completed = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
reactionRecognizer.activateReply = { [weak self] in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
var bounds = strongSelf.bounds
|
||||
@ -454,6 +461,56 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
if !offset.isZero {
|
||||
strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
if let swipeToReplyNode = strongSelf.swipeToReplyNode {
|
||||
strongSelf.swipeToReplyNode = nil
|
||||
swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in
|
||||
swipeToReplyNode?.removeFromSupernode()
|
||||
})
|
||||
swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
}
|
||||
item.controllerInteraction.setupReply(item.message.id)
|
||||
}
|
||||
reactionRecognizer.displayReply = { [weak self] offset in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
if strongSelf.swipeToReplyNode == nil {
|
||||
if strongSelf.swipeToReplyFeedback == nil {
|
||||
strongSelf.swipeToReplyFeedback = HapticFeedback()
|
||||
}
|
||||
strongSelf.swipeToReplyFeedback?.tap()
|
||||
let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonFillColor, wallpaper: item.presentationData.theme.wallpaper), strokeColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonStrokeColor, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper))
|
||||
strongSelf.swipeToReplyNode = swipeToReplyNode
|
||||
strongSelf.insertSubnode(swipeToReplyNode, belowSubnode: strongSelf.messageAccessibilityArea)
|
||||
swipeToReplyNode.frame = CGRect(origin: CGPoint(x: strongSelf.bounds.size.width, y: floor((strongSelf.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0))
|
||||
swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12)
|
||||
swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||
}
|
||||
}
|
||||
reactionRecognizer.completed = { [weak self] reaction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let item = strongSelf.item, let reaction = reaction {
|
||||
strongSelf.awaitingAppliedReaction = reaction.value.value
|
||||
item.controllerInteraction.updateMessageReaction(item.message.id, reaction.value.value)
|
||||
} else {
|
||||
strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false)
|
||||
var bounds = strongSelf.bounds
|
||||
let offset = bounds.origin.x
|
||||
bounds.origin.x = 0.0
|
||||
strongSelf.bounds = bounds
|
||||
if !offset.isZero {
|
||||
strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
if let swipeToReplyNode = strongSelf.swipeToReplyNode {
|
||||
strongSelf.swipeToReplyNode = nil
|
||||
swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in
|
||||
swipeToReplyNode?.removeFromSupernode()
|
||||
})
|
||||
swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.view.addGestureRecognizer(reactionRecognizer)
|
||||
}
|
||||
@ -512,7 +569,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, CGSize) -> (CGSize, () -> ChatMessageForwardInfoNode),
|
||||
replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode),
|
||||
actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)),
|
||||
mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode),
|
||||
mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize, [MessageReaction]) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode),
|
||||
currentShareButtonNode: HighlightableButtonNode?,
|
||||
layoutConstants: ChatMessageItemLayoutConstants,
|
||||
currentItem: ChatMessageItem?,
|
||||
@ -956,7 +1013,20 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings)
|
||||
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: message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount)
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType
|
||||
if message.effectivelyIncoming(item.context.account.peerId) {
|
||||
@ -971,7 +1041,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), dateReactions)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1843,6 +1913,35 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
strongSelf.updateSearchTextHighlightState()
|
||||
|
||||
if let awaitingAppliedReaction = strongSelf.awaitingAppliedReaction {
|
||||
var bounds = strongSelf.bounds
|
||||
let offset = bounds.origin.x
|
||||
bounds.origin.x = 0.0
|
||||
strongSelf.bounds = bounds
|
||||
if !offset.isZero {
|
||||
strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
if let swipeToReplyNode = strongSelf.swipeToReplyNode {
|
||||
strongSelf.swipeToReplyNode = nil
|
||||
swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in
|
||||
swipeToReplyNode?.removeFromSupernode()
|
||||
})
|
||||
swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
strongSelf.awaitingAppliedReaction = nil
|
||||
var targetNode: ASImageNode?
|
||||
var hideTarget = false
|
||||
for contentNode in strongSelf.contentNodes {
|
||||
if let (reactionNode, count) = contentNode.reactionTargetNode(value: awaitingAppliedReaction) {
|
||||
targetNode = reactionNode
|
||||
hideTarget = count == 1
|
||||
break
|
||||
}
|
||||
}
|
||||
strongSelf.reactionRecognizer?.complete(into: targetNode, hideTarget: hideTarget)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
|
||||
|
@ -128,7 +128,7 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
buttonImage = PresentationResourcesChat.chatBubbleOutgoingCallButtonImage(item.presentationData.theme.theme)
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings)
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: 0)
|
||||
|
||||
let statusText: String
|
||||
if let callDuration = callDuration, callDuration > 1 {
|
||||
@ -214,4 +214,8 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -149,7 +149,20 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings)
|
||||
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 {
|
||||
@ -173,7 +186,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
|
||||
if let statusType = statusType {
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions)
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
@ -330,4 +343,11 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||
}
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionNode(value: value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
@ -104,6 +105,28 @@ enum ChatMessageDateAndStatusType: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private let reactionSize: CGFloat = 18.0
|
||||
|
||||
private final class StatusReactionNode: ASImageNode {
|
||||
let value: String
|
||||
var count: Int
|
||||
|
||||
init(value: String, count: Int) {
|
||||
self.value = value
|
||||
self.count = count
|
||||
|
||||
super.init()
|
||||
|
||||
self.image = generateImage(CGSize(width: reactionSize, height: reactionSize), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPushContext(context)
|
||||
let string = NSAttributedString(string: value, font: Font.regular(11.0), textColor: .black)
|
||||
string.draw(at: CGPoint(x: 1.0, y: 3.0))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
private var backgroundNode: ASImageNode?
|
||||
private var checkSentNode: ASImageNode?
|
||||
@ -112,6 +135,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
private var clockMinNode: ASImageNode?
|
||||
private let dateNode: TextNode
|
||||
private var impressionIcon: ASImageNode?
|
||||
private var reactionNodes: [StatusReactionNode] = []
|
||||
|
||||
private var type: ChatMessageDateAndStatusType?
|
||||
private var theme: ChatPresentationThemeData?
|
||||
@ -128,7 +152,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
self.addSubnode(self.dateNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, (Bool) -> Void) {
|
||||
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction]) -> (CGSize, (Bool) -> Void) {
|
||||
let dateLayout = TextNode.asyncLayout(self.dateNode)
|
||||
|
||||
var checkReadNode = self.checkReadNode
|
||||
@ -142,11 +166,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
let currentType = self.type
|
||||
let currentTheme = self.theme
|
||||
|
||||
return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize in
|
||||
return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions in
|
||||
let dateColor: UIColor
|
||||
var backgroundImage: UIImage?
|
||||
var outgoingStatus: ChatMessageDateAndStatusOutgoingType?
|
||||
let leftInset: CGFloat
|
||||
var leftInset: CGFloat
|
||||
|
||||
let loadedCheckFullImage: UIImage?
|
||||
let loadedCheckPartialImage: UIImage?
|
||||
@ -372,6 +396,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0)
|
||||
}
|
||||
|
||||
var reactionInset: CGFloat = 0.0
|
||||
if !reactions.isEmpty {
|
||||
reactionInset = 1.0 + CGFloat(reactions.count) * reactionSize
|
||||
}
|
||||
leftInset += reactionInset
|
||||
|
||||
let layoutSize = CGSize(width: leftInset + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
|
||||
|
||||
return (layoutSize, { [weak self] animated in
|
||||
@ -423,7 +453,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
} else if themeUpdated {
|
||||
clockFrameNode.image = clockFrameImage
|
||||
}
|
||||
clockFrameNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x, y: backgroundInsets.top + clockPosition.y)
|
||||
clockFrameNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y)
|
||||
if let clockFrameNode = strongSelf.clockFrameNode {
|
||||
maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0)
|
||||
}
|
||||
@ -440,7 +470,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
} else if themeUpdated {
|
||||
clockMinNode.image = clockMinImage
|
||||
}
|
||||
clockMinNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x, y: backgroundInsets.top + clockPosition.y)
|
||||
clockMinNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y)
|
||||
if let clockMinNode = strongSelf.clockMinNode {
|
||||
maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0)
|
||||
}
|
||||
@ -465,7 +495,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
animateSentNode = animated
|
||||
}
|
||||
checkSentNode.isHidden = false
|
||||
checkSentNode.frame = checkSentFrame.offsetBy(dx: backgroundInsets.left, dy: backgroundInsets.top)
|
||||
checkSentNode.frame = checkSentFrame.offsetBy(dx: backgroundInsets.left + reactionInset, dy: backgroundInsets.top)
|
||||
} else {
|
||||
checkSentNode.isHidden = true
|
||||
}
|
||||
@ -485,7 +515,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
animateReadNode = animated
|
||||
}
|
||||
checkReadNode.isHidden = false
|
||||
checkReadNode.frame = checkReadFrame.offsetBy(dx: backgroundInsets.left, dy: backgroundInsets.top)
|
||||
checkReadNode.frame = checkReadFrame.offsetBy(dx: backgroundInsets.left + reactionInset, dy: backgroundInsets.top)
|
||||
} else {
|
||||
checkReadNode.isHidden = true
|
||||
}
|
||||
@ -502,22 +532,47 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
strongSelf.checkSentNode = nil
|
||||
strongSelf.checkReadNode = nil
|
||||
}
|
||||
|
||||
var reactionOffset: CGFloat = leftInset - reactionInset + backgroundInsets.left
|
||||
for i in 0 ..< reactions.count {
|
||||
let node: StatusReactionNode
|
||||
if strongSelf.reactionNodes.count > i, strongSelf.reactionNodes[i].value == reactions[i].value {
|
||||
node = strongSelf.reactionNodes[i]
|
||||
node.count = Int(reactions[i].count)
|
||||
} else {
|
||||
node = StatusReactionNode(value: reactions[i].value, count: Int(reactions[i].count))
|
||||
if strongSelf.reactionNodes.count > i {
|
||||
strongSelf.reactionNodes[i].removeFromSupernode()
|
||||
strongSelf.reactionNodes[i] = node
|
||||
} else {
|
||||
strongSelf.reactionNodes.append(node)
|
||||
}
|
||||
}
|
||||
if node.supernode == nil {
|
||||
strongSelf.addSubnode(node)
|
||||
}
|
||||
node.frame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + 1.0 + offset - 3.0), size: CGSize(width: reactionSize, height: reactionSize))
|
||||
reactionOffset += reactionSize
|
||||
}
|
||||
for _ in reactions.count ..< strongSelf.reactionNodes.count {
|
||||
strongSelf.reactionNodes.removeLast().removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) {
|
||||
static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction]) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) {
|
||||
let currentLayout = node?.asyncLayout()
|
||||
return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize in
|
||||
return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions in
|
||||
let resultNode: ChatMessageDateAndStatusNode
|
||||
let resultSizeAndApply: (CGSize, (Bool) -> Void)
|
||||
if let node = node, let currentLayout = currentLayout {
|
||||
resultNode = node
|
||||
resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize)
|
||||
resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions)
|
||||
} else {
|
||||
resultNode = ChatMessageDateAndStatusNode()
|
||||
resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize)
|
||||
resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions)
|
||||
}
|
||||
|
||||
return (resultSizeAndApply.0, { animated in
|
||||
@ -526,4 +581,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func reactionNode(value: String) -> (ASImageNode, Int)? {
|
||||
for node in self.reactionNodes {
|
||||
if node.value == value {
|
||||
return (node, node.count)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -117,4 +117,8 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.interactiveFileNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
return self.interactiveFileNode.reactionTargetNode(value: value)
|
||||
}
|
||||
}
|
||||
|
@ -132,4 +132,8 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
return self.contentNode.transitionNode(media: media)
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
return self.contentNode.reactionTargetNode(value: value)
|
||||
}
|
||||
}
|
||||
|
@ -276,9 +276,22 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings)
|
||||
var dateReactions: [MessageReaction] = []
|
||||
var dateReactionCount = 0
|
||||
if let reactionsAttribute = mergedMessageReactions(attributes: 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 (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize)
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, reactionCount: dateReactionCount)
|
||||
|
||||
let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize, dateReactions)
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
@ -927,4 +940,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
self.playerUpdateTimer?.invalidate()
|
||||
self.playerUpdateTimer = nil
|
||||
}
|
||||
|
||||
func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionNode(value: value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -246,23 +246,31 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
let edited = false
|
||||
var edited = false
|
||||
let sentViaBot = false
|
||||
var viewCount: Int? = nil
|
||||
for attribute in item.message.attributes {
|
||||
if let _ = attribute as? EditedMessageAttribute {
|
||||
// edited = true
|
||||
if let attribute = attribute as? EditedMessageAttribute {
|
||||
edited = !attribute.isHidden
|
||||
} else if let attribute = attribute as? ViewCountMessageAttribute {
|
||||
viewCount = attribute.count
|
||||
}// else if let _ = attribute as? InlineBotMessageAttribute {
|
||||
// sentViaBot = true
|
||||
// }
|
||||
}
|
||||
}
|
||||
// if let author = item.message.author as? TelegramUser, author.botInfo != nil {
|
||||
// sentViaBot = true
|
||||
// }
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular)
|
||||
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, format: .regular, reactionCount: dateReactionCount)
|
||||
|
||||
let maxDateAndStatusWidth: CGFloat
|
||||
if case .bubble = statusDisplayType {
|
||||
@ -270,7 +278,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
} else {
|
||||
maxDateAndStatusWidth = width - videoFrame.midX - 85.0
|
||||
}
|
||||
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude))
|
||||
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReactions)
|
||||
|
||||
var contentSize = imageSize
|
||||
var dateAndStatusOverflow = false
|
||||
|
@ -135,4 +135,8 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
return self.contentNode.transitionNode(media: media)
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
return self.contentNode.reactionTargetNode(value: value)
|
||||
}
|
||||
}
|
||||
|
@ -181,13 +181,26 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if let selectedMedia = selectedMedia {
|
||||
if selectedMedia.liveBroadcastingTimeout != nil {
|
||||
edited = false
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings)
|
||||
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 {
|
||||
@ -225,7 +238,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
|
||||
if let statusType = statusType {
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions)
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
@ -455,4 +468,11 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionNode(value: value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -164,7 +164,20 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings)
|
||||
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 {
|
||||
@ -192,7 +205,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
|
||||
if let statusType = statusType {
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateReactions)
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
@ -359,4 +372,11 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionNode(value: value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -596,7 +596,20 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings)
|
||||
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 {
|
||||
@ -620,7 +633,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
|
||||
if let statusType = statusType {
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize)
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions)
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
@ -942,4 +955,11 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
if !self.statusNode.isHidden {
|
||||
return self.statusNode.reactionNode(value: value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,20 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings)
|
||||
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 {
|
||||
@ -83,7 +96,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
|
||||
if let statusType = statusType {
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize)
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions)
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
@ -95,7 +108,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let textFont = item.presentationData.messageFont
|
||||
let forceStatusNewline = false
|
||||
|
||||
let attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont)
|
||||
let attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor.withAlphaComponent(0.7), linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont)
|
||||
|
||||
var cutout: TextNodeCutout?
|
||||
if let statusSize = statusSize, !forceStatusNewline {
|
||||
@ -230,4 +243,11 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
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 reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
if !self.statusNode.isHidden {
|
||||
return self.statusNode.reactionNode(value: value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -265,9 +265,22 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular)
|
||||
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 (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, reactionCount: dateReactionCount)
|
||||
|
||||
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions)
|
||||
|
||||
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
||||
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
|
||||
|
@ -106,7 +106,20 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings)
|
||||
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 {
|
||||
@ -130,7 +143,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
|
||||
if let statusType = statusType {
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize)
|
||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions)
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
@ -158,20 +171,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
rawText = item.presentationData.strings.Conversation_UnsupportedMediaPlaceholder
|
||||
messageEntities = [MessageTextEntity(range: 0..<rawText.count, type: .Italic)]
|
||||
} else {
|
||||
var reactionsString = ""
|
||||
if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty {
|
||||
reactionsString += "\n\nReactions:"
|
||||
|
||||
for reaction in reactionsAttribute.reactions {
|
||||
reactionsString += "\n[\(reaction.value)"
|
||||
if reaction.isSelected {
|
||||
reactionsString += "✓"
|
||||
}
|
||||
reactionsString += "]"
|
||||
}
|
||||
}
|
||||
|
||||
rawText = item.message.text + reactionsString
|
||||
rawText = item.message.text
|
||||
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
@ -540,4 +540,11 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
if !self.statusNode.isHidden {
|
||||
return self.statusNode.reactionNode(value: value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -410,6 +410,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, sendScheduledMessagesNow: { _ in
|
||||
}, editScheduledMessagesTime: { _ in
|
||||
}, performTextSelectionAction: { _, _, _ in
|
||||
}, updateMessageReaction: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
|
@ -112,6 +112,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, sendScheduledMessagesNow: { _ in
|
||||
}, editScheduledMessagesTime: { _ in
|
||||
}, performTextSelectionAction: { _, _, _ in
|
||||
}, updateMessageReaction: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))
|
||||
|
@ -286,6 +286,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
|
||||
}, sendScheduledMessagesNow: { _ in
|
||||
}, editScheduledMessagesTime: { _ in
|
||||
}, performTextSelectionAction: { _, _, _ in
|
||||
}, updateMessageReaction: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
@ -11,7 +11,7 @@ enum MessageTimestampStatusFormat {
|
||||
case minimal
|
||||
}
|
||||
|
||||
func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular) -> String {
|
||||
func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular, reactionCount: Int) -> String {
|
||||
let timestamp: Int32
|
||||
if let scheduleTime = message.scheduleTime {
|
||||
timestamp = scheduleTime
|
||||
|
Loading…
x
Reference in New Issue
Block a user