Initial reactions proof of concept

This commit is contained in:
Peter 2019-08-16 23:16:14 +03:00
parent ae165ed4a5
commit 9bad9b86ab
28 changed files with 623 additions and 84 deletions

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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)?

View File

@ -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
}
}

View File

@ -172,4 +172,8 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
func updateIsExtractedToContextPreview(_ value: Bool) {
}
func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
return nil
}
}

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)?

View File

@ -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
}
}

View File

@ -410,6 +410,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, sendScheduledMessagesNow: { _ in
}, editScheduledMessagesTime: { _ in
}, performTextSelectionAction: { _, _, _ in
}, updateMessageReaction: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

@ -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))

View File

@ -286,6 +286,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
}, sendScheduledMessagesNow: { _ in
}, editScheduledMessagesTime: { _ in
}, performTextSelectionAction: { _, _, _ in
}, updateMessageReaction: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -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