Reaction improvements

This commit is contained in:
Ali 2022-09-06 17:26:14 +04:00
parent afcf6a89d0
commit 007ed84bc3
10 changed files with 299 additions and 118 deletions

View File

@ -33,6 +33,7 @@ swift_library(
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/TextFormat:TextFormat",
"//submodules/GZip:GZip",
"//submodules/ShimmerEffect:ShimmerEffect",
],
visibility = [
"//visibility:public",

View File

@ -1423,13 +1423,16 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
let expandedFrame = CGRect(origin: CGPoint(x: selfTargetRect.midX - expandedSize.width / 2.0, y: selfTargetRect.midY - expandedSize.height / 2.0), size: expandedSize)
let effectFrame: CGRect
var effectFrame: CGRect
let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0
if self.didTriggerExpandedReaction {
let expandFactor: CGFloat = 0.5
effectFrame = expandedFrame.insetBy(dx: -expandedFrame.width * expandFactor, dy: -expandedFrame.height * expandFactor).offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0)
} else {
effectFrame = expandedFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height)
if itemNode.item.isCustom {
effectFrame = effectFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height)
}
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear)
@ -1442,11 +1445,28 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
let additionalAnimationNode: DefaultAnimatedStickerNodeImpl?
var genericAnimationView: AnimationView?
let additionalAnimation: TelegramMediaFile?
var additionalAnimation: TelegramMediaFile?
if self.didTriggerExpandedReaction {
additionalAnimation = itemNode.item.largeApplicationAnimation
} else {
additionalAnimation = itemNode.item.applicationAnimation
if additionalAnimation == nil && itemNode.item.isCustom {
outer: for attribute in itemNode.item.stillAnimation.attributes {
if case let .CustomEmoji(_, alt, _) = attribute {
if let availableReactions = self.availableReactions {
for availableReaction in availableReactions.reactions {
if availableReaction.value == .builtin(alt) {
additionalAnimation = availableReaction.aroundAnimation
break outer
}
}
}
break
}
}
}
}
if let additionalAnimation = additionalAnimation {
@ -1676,6 +1696,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
recognizer.state = .cancelled
return
}
if !itemNode.isAnimationLoaded {
recognizer.state = .cancelled
return
}
self.highlightedReaction = itemNode.item.reaction
if #available(iOS 13.0, *) {
@ -1871,6 +1895,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
public func reaction(at point: CGPoint) -> ReactionContextItem? {
let itemNode = self.reactionItemNode(at: point)
if let itemNode = itemNode as? ReactionNode {
if !itemNode.isAnimationLoaded {
return nil
}
return .reaction(itemNode.item)
} else if let _ = itemNode as? PremiumReactionsNode {
return .premium

View File

@ -12,6 +12,7 @@ import StickerResources
import AccountContext
import AnimationCache
import MultiAnimationRenderer
import ShimmerEffect
private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in
@ -48,6 +49,7 @@ protocol ReactionItemNode: ASDisplayNode {
public final class ReactionNode: ASDisplayNode, ReactionItemNode {
let context: AccountContext
let theme: PresentationTheme
let item: ReactionItem
private let loopIdle: Bool
private let hasAppearAnimation: Bool
@ -57,6 +59,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
let selectionView: UIView
private var animateInAnimationNode: AnimatedStickerNode?
private var staticAnimationPlaceholderView: UIImageView?
private let staticAnimationNode: AnimatedStickerNode
private var stillAnimationNode: AnimatedStickerNode?
private var customContentsNode: ASDisplayNode?
@ -83,8 +86,13 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
return self.staticAnimationNode.currentFrameImage
}
var isAnimationLoaded: Bool {
return self.staticAnimationNode.currentFrameImage != nil
}
public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, loopIdle: Bool, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) {
self.context = context
self.theme = theme
self.item = item
self.loopIdle = loopIdle
self.hasAppearAnimation = hasAppearAnimation
@ -361,6 +369,33 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
if self.animationNode == nil {
self.didSetupStillAnimation = true
let staticFile: TelegramMediaFile
if !self.hasAppearAnimation {
staticFile = self.item.largeListAnimation
} else {
staticFile = self.item.stillAnimation
}
if self.staticAnimationPlaceholderView == nil, let immediateThumbnailData = staticFile.immediateThumbnailData {
let staticAnimationPlaceholderView = UIImageView()
self.view.addSubview(staticAnimationPlaceholderView)
self.staticAnimationPlaceholderView = staticAnimationPlaceholderView
if let image = generateStickerPlaceholderImage(data: immediateThumbnailData, size: animationDisplaySize, scale: min(2.0, UIScreenScale), imageSize: staticFile.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: self.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1)) {
staticAnimationPlaceholderView.image = image
}
}
self.staticAnimationNode.started = { [weak self] in
guard let strongSelf = self else {
return
}
if let staticAnimationPlaceholderView = strongSelf.staticAnimationPlaceholderView {
strongSelf.staticAnimationPlaceholderView = nil
staticAnimationPlaceholderView.removeFromSuperview()
}
}
self.staticAnimationNode.automaticallyLoadFirstFrame = true
if !self.hasAppearAnimation {
self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource, isVideo: self.item.largeListAnimation.isVideoEmoji || self.item.largeListAnimation.isVideoSticker || self.item.largeListAnimation.isStaticSticker || self.item.largeListAnimation.isStaticEmoji), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation.resource.id)))
@ -372,6 +407,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
self.staticAnimationNode.updateLayout(size: animationFrame.size)
self.staticAnimationNode.visibility = true
if let staticAnimationPlaceholderView = self.staticAnimationPlaceholderView {
staticAnimationPlaceholderView.center = animationFrame.center
staticAnimationPlaceholderView.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
}
if let animateInAnimationNode = self.animateInAnimationNode {
animateInAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.appearAnimation.resource, isVideo: self.item.appearAnimation.isVideoEmoji || self.item.appearAnimation.isVideoSticker || self.item.appearAnimation.isStaticSticker || self.item.appearAnimation.isStaticEmoji), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.appearAnimation.resource.id)))
animateInAnimationNode.position = animationFrame.center
@ -383,6 +423,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
transition.updatePosition(node: self.staticAnimationNode, position: animationFrame.center, beginWithCurrentState: true)
transition.updateTransformScale(node: self.staticAnimationNode, scale: animationFrame.size.width / self.staticAnimationNode.bounds.width, beginWithCurrentState: true)
if let staticAnimationPlaceholderView = self.staticAnimationPlaceholderView {
transition.updatePosition(layer: staticAnimationPlaceholderView.layer, position: animationFrame.center)
transition.updateTransformScale(layer: staticAnimationPlaceholderView.layer, scale: animationFrame.size.width / self.staticAnimationNode.bounds.width)
}
if let animateInAnimationNode = self.animateInAnimationNode {
transition.updatePosition(node: animateInAnimationNode, position: animationFrame.center, beginWithCurrentState: true)
transition.updateTransformScale(node: animateInAnimationNode, scale: animationFrame.size.width / animateInAnimationNode.bounds.width, beginWithCurrentState: true)

View File

@ -66,12 +66,17 @@ public func mergedMessageReactionsAndPeers(accountPeer: EnginePeer?, message: Me
if message.id.peerId.namespace == Namespaces.Peer.CloudUser {
for reaction in attribute.reactions {
var selfCount: Int32 = 0
if reaction.isSelected {
selfCount += 1
if let accountPeer = accountPeer {
recentPeers.append((reaction.value, accountPeer))
}
} else if let peer = message.peers[message.id.peerId] {
recentPeers.append((reaction.value, EnginePeer(peer)))
}
if reaction.count > selfCount + 1 {
if let peer = message.peers[message.id.peerId] {
recentPeers.append((reaction.value, EnginePeer(peer)))
}
}
}
} else {

View File

@ -1629,6 +1629,15 @@ func extractEmojiFileIds(message: StoreMessage, fileIds: inout Set<Int64>) {
break
}
}
} else if let attribute = attribute as? ReactionsMessageAttribute {
for reaction in attribute.reactions {
switch reaction.value {
case let .custom(fileId):
fileIds.insert(fileId)
default:
break
}
}
}
}
}
@ -1648,10 +1657,22 @@ private func messagesFromOperations(state: AccountMutableState) -> [StoreMessage
return messages
}
private func reactionsFromState(_ state: AccountMutableState) -> [MessageReaction.Reaction] {
var result: [MessageReaction.Reaction] = []
for operation in state.operations {
if case let .UpdateMessageReactions(_, reactions, _) = operation {
for reaction in ReactionsMessageAttribute(apiReactions: reactions).reactions {
result.append(reaction.value)
}
}
}
return result
}
private func resolveAssociatedMessages(postbox: Postbox, network: Network, state: AccountMutableState) -> Signal<AccountMutableState, NoError> {
let missingMessageIds = state.referencedMessageIds.subtracting(state.storedMessages)
if missingMessageIds.isEmpty {
return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: messagesFromOperations(state: state), result: state)
return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: messagesFromOperations(state: state), reactions: reactionsFromState(state), result: state)
} else {
var missingPeers = false
let _ = missingPeers
@ -1713,7 +1734,7 @@ private func resolveAssociatedMessages(postbox: Postbox, network: Network, state
return updatedState
}
|> mapToSignal { updatedState -> Signal<AccountMutableState, NoError> in
return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: messagesFromOperations(state: updatedState), result: updatedState)
return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: messagesFromOperations(state: updatedState), reactions: reactionsFromState(updatedState), result: updatedState)
}
}
}

View File

@ -390,7 +390,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo
folderSummaries: folderSummaries,
peerGroupIds: peerGroupIds
)
return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: storeMessages, result: result)
return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: storeMessages, reactions: [], result: result)
}
}
}

View File

@ -43,13 +43,19 @@ enum FetchMessageHistoryHoleSource {
}
}
func resolveUnknownEmojiFiles<T>(postbox: Postbox, source: FetchMessageHistoryHoleSource, messages: [StoreMessage], result: T) -> Signal<T, NoError> {
func resolveUnknownEmojiFiles<T>(postbox: Postbox, source: FetchMessageHistoryHoleSource, messages: [StoreMessage], reactions: [MessageReaction.Reaction], result: T) -> Signal<T, NoError> {
var fileIds = Set<Int64>()
for message in messages {
extractEmojiFileIds(message: message, fileIds: &fileIds)
}
for reaction in reactions {
if case let .custom(fileId) = reaction {
fileIds.insert(fileId)
}
}
if fileIds.isEmpty {
return .single(result)
} else {
@ -111,7 +117,7 @@ private func withResolvedAssociatedMessages<T>(postbox: Postbox, source: FetchMe
referencedIds.subtract(transaction.filterStoredMessageIds(referencedIds))
if referencedIds.isEmpty {
return resolveUnknownEmojiFiles(postbox: postbox, source: source, messages: storeMessages, result: Void())
return resolveUnknownEmojiFiles(postbox: postbox, source: source, messages: storeMessages, reactions: [], result: Void())
|> mapToSignal { _ -> Signal<T, NoError> in
return postbox.transaction { transaction -> T in
return f(transaction, [], [])
@ -174,7 +180,7 @@ private func withResolvedAssociatedMessages<T>(postbox: Postbox, source: FetchMe
}
}
return resolveUnknownEmojiFiles(postbox: postbox, source: source, messages: storeMessages + additionalMessages, result: Void())
return resolveUnknownEmojiFiles(postbox: postbox, source: source, messages: storeMessages + additionalMessages, reactions: [], result: Void())
|> mapToSignal { _ -> Signal<T, NoError> in
return postbox.transaction { transaction -> T in
return f(transaction, additionalPeers, additionalMessages)

View File

@ -1390,6 +1390,12 @@ private func findHigherResolutionFileForAdaptation(itemDirectoryPath: String, ba
public final class AnimationCacheImpl: AnimationCache {
private final class Impl {
private struct ItemKey: Hashable {
var id: String
var width: Int
var height: Int
}
private final class ItemContext {
let subscribers = Bag<(AnimationCacheItemResult) -> Void>()
let disposable = MetaDisposable()
@ -1406,7 +1412,7 @@ public final class AnimationCacheImpl: AnimationCache {
private let fetchQueues: [Queue]
private var nextFetchQueueIndex: Int = 0
private var itemContexts: [String: ItemContext] = [:]
private var itemContexts: [ItemKey: ItemContext] = [:]
init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String) {
self.queue = queue
@ -1437,14 +1443,15 @@ public final class AnimationCacheImpl: AnimationCache {
return EmptyDisposable
}
let key = ItemKey(id: sourceId, width: Int(size.width), height: Int(size.height))
let itemContext: ItemContext
var beginFetch = false
if let current = self.itemContexts[sourceId] {
if let current = self.itemContexts[key] {
itemContext = current
} else {
itemContext = ItemContext()
self.itemContexts[sourceId] = itemContext
self.itemContexts[key] = itemContext
beginFetch = true
}
@ -1459,11 +1466,11 @@ public final class AnimationCacheImpl: AnimationCache {
let allocateTempFile = self.allocateTempFile
guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in
queue.async {
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[sourceId] else {
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else {
return
}
strongSelf.itemContexts.removeValue(forKey: sourceId)
strongSelf.itemContexts.removeValue(forKey: key)
guard let result = result else {
return
@ -1503,13 +1510,13 @@ public final class AnimationCacheImpl: AnimationCache {
return ActionDisposable { [weak self, weak itemContext] in
queue.async {
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[sourceId] else {
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else {
return
}
itemContext.subscribers.remove(index)
if itemContext.subscribers.isEmpty {
itemContext.disposable.dispose()
strongSelf.itemContexts.removeValue(forKey: sourceId)
strongSelf.itemContexts.removeValue(forKey: key)
}
}
}

View File

@ -6718,6 +6718,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch result {
case let .result(messageId):
if let messageId = messageId {
strongSelf.chatDisplayNode.historyNode.suspendReadingReactions = true
strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), scrollPosition: .center(.top), completion: {
guard let strongSelf = self else {
return
@ -6743,34 +6744,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
guard let availableReactions = item.associatedData.availableReactions else {
return
}
var avatarPeers: [EnginePeer] = []
if item.message.id.peerId.namespace != Namespaces.Peer.CloudUser, let updatedReactionPeer = updatedReactionPeer {
avatarPeers.append(updatedReactionPeer)
}
guard let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) else {
return
}
for reaction in availableReactions.reactions {
guard let centerAnimation = reaction.centerAnimation else {
continue
}
guard let aroundAnimation = reaction.aroundAnimation else {
continue
}
if reaction.value == updatedReaction {
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: strongSelf.chatDisplayNode.historyNode.takeGenericReactionEffect())
strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)
standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds
standaloneReactionAnimation.animateReactionSelection(
context: strongSelf.context,
theme: strongSelf.presentationData.theme,
animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache,
reaction: ReactionItem(
var reactionItem: ReactionItem?
switch updatedReaction {
case .builtin:
for reaction in availableReactions.reactions {
guard let centerAnimation = reaction.centerAnimation else {
continue
}
guard let aroundAnimation = reaction.aroundAnimation else {
continue
}
if reaction.value == updatedReaction {
reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: reaction.value),
appearAnimation: reaction.appearAnimation,
stillAnimation: reaction.selectAnimation,
@ -6779,28 +6774,60 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
applicationAnimation: aroundAnimation,
largeApplicationAnimation: reaction.effectAnimation,
isCustom: false
),
avatarPeers: avatarPeers,
playHaptic: true,
isLarge: updatedReactionIsLarge,
targetView: targetView,
addStandaloneReactionAnimation: { standaloneReactionAnimation in
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds
strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)
},
completion: { [weak standaloneReactionAnimation] in
standaloneReactionAnimation?.removeFromSupernode()
}
)
break
}
}
case let .custom(fileId):
if let itemFile = item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile {
reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: updatedReaction),
appearAnimation: itemFile,
stillAnimation: itemFile,
listAnimation: itemFile,
largeListAnimation: itemFile,
applicationAnimation: nil,
largeApplicationAnimation: nil,
isCustom: true
)
break
}
}
guard let targetView = itemNode.targetReactionView(value: updatedReaction) else {
return
}
if let reactionItem = reactionItem {
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: strongSelf.chatDisplayNode.historyNode.takeGenericReactionEffect())
strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)
standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds
standaloneReactionAnimation.animateReactionSelection(
context: strongSelf.context,
theme: strongSelf.presentationData.theme,
animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache,
reaction: reactionItem,
avatarPeers: avatarPeers,
playHaptic: true,
isLarge: updatedReactionIsLarge,
targetView: targetView,
addStandaloneReactionAnimation: { standaloneReactionAnimation in
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds
strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)
},
completion: { [weak standaloneReactionAnimation] in
standaloneReactionAnimation?.removeFromSupernode()
}
)
}
}
strongSelf.chatDisplayNode.historyNode.suspendReadingReactions = false
})
}
case .loading:
@ -17049,20 +17076,26 @@ func peerMessageAllowedReactions(context: AccountContext, message: Message) -> S
return .set(Set(effectiveReactions.map(\.value)))
}
if case let .channel(channel) = peer, case .broadcast = channel.info {
if let availableReactions = availableReactions {
return .set(Set(availableReactions.reactions.map(\.value)))
} else {
return .set(Set())
}
}
switch allowedReactions {
case .unknown:
if case let .channel(channel) = peer, case .broadcast = channel.info {
if let availableReactions = availableReactions {
return .set(Set(availableReactions.reactions.map(\.value)))
} else {
return .set(Set())
}
}
return .all
case let .known(value):
switch value {
case .all:
if case let .channel(channel) = peer, case .broadcast = channel.info {
if let availableReactions = availableReactions {
return .set(Set(availableReactions.reactions.map(\.value)))
} else {
return .set(Set())
}
}
return .all
case let .limited(reactions):
return .set(Set(reactions))

View File

@ -451,6 +451,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let canReadHistory = Promise<Bool>()
private var canReadHistoryValue: Bool = false
private var canReadHistoryDisposable: Disposable?
var suspendReadingReactions: Bool = false {
didSet {
if self.suspendReadingReactions != oldValue {
if !self.suspendReadingReactions {
self.attemptReadingReactions()
}
}
}
}
private var messageIdsScheduledForMarkAsSeen = Set<MessageId>()
private var messageIdsWithReactionsScheduledForMarkAsSeen = Set<MessageId>()
@ -728,7 +738,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
guard let strongSelf = self else {
return
}
if strongSelf.canReadHistoryValue && !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory {
if strongSelf.canReadHistoryValue && !strongSelf.suspendReadingReactions && !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory {
strongSelf.context.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: messageIds)
} else {
strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.formUnion(messageIds)
@ -1365,20 +1375,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
strongSelf.canReadHistoryValue = value
strongSelf.updateReadHistoryActions()
if strongSelf.canReadHistoryValue && !strongSelf.messageIdsScheduledForMarkAsSeen.isEmpty {
if strongSelf.canReadHistoryValue && !strongSelf.suspendReadingReactions && !strongSelf.messageIdsScheduledForMarkAsSeen.isEmpty {
let messageIds = strongSelf.messageIdsScheduledForMarkAsSeen
strongSelf.messageIdsScheduledForMarkAsSeen.removeAll()
context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds)
}
if strongSelf.canReadHistoryValue && !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.isEmpty {
let messageIds = strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen
let _ = strongSelf.displayUnseenReactionAnimations(messageIds: Array(messageIds))
strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.removeAll()
context?.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: messageIds)
}
strongSelf.attemptReadingReactions()
}
}
})
@ -1598,6 +1601,17 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.genericReactionEffectDisposable?.dispose()
}
private func attemptReadingReactions() {
if self.canReadHistoryValue && !self.suspendReadingReactions && !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !self.messageIdsWithReactionsScheduledForMarkAsSeen.isEmpty {
let messageIds = self.messageIdsWithReactionsScheduledForMarkAsSeen
let _ = self.displayUnseenReactionAnimations(messageIds: Array(messageIds))
self.messageIdsWithReactionsScheduledForMarkAsSeen.removeAll()
self.context.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: messageIds)
}
}
func takeGenericReactionEffect() -> String? {
let result = self.genericReactionEffect
self.loadNextGenericReactionEffect(context: self.context)
@ -2754,32 +2768,20 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
visibleNewIncomingReactionMessageIds.append(item.content.firstMessage.id)
if let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) {
for reaction in availableReactions.reactions {
guard let centerAnimation = reaction.centerAnimation else {
continue
}
guard let aroundAnimation = reaction.aroundAnimation else {
continue
}
if reaction.value == updatedReaction {
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: self.genericReactionEffect)
chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
var avatarPeers: [EnginePeer] = []
if item.message.id.peerId.namespace != Namespaces.Peer.CloudUser, let updateReactionPeer = updateReactionPeer {
avatarPeers = [updateReactionPeer]
var reactionItem: ReactionItem?
switch updatedReaction {
case .builtin:
if let availableReactions = item.associatedData.availableReactions {
for reaction in availableReactions.reactions {
guard let centerAnimation = reaction.centerAnimation else {
continue
}
chatDisplayNode.addSubnode(standaloneReactionAnimation)
standaloneReactionAnimation.frame = chatDisplayNode.bounds
standaloneReactionAnimation.animateReactionSelection(
context: self.context,
theme: item.presentationData.theme.theme,
animationCache: self.controllerInteraction.presentationContext.animationCache,
reaction: ReactionItem(
guard let aroundAnimation = reaction.aroundAnimation else {
continue
}
if reaction.value == updatedReaction {
reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: reaction.value),
appearAnimation: reaction.appearAnimation,
stillAnimation: reaction.selectAnimation,
@ -2788,25 +2790,59 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
applicationAnimation: aroundAnimation,
largeApplicationAnimation: reaction.effectAnimation,
isCustom: false
),
avatarPeers: avatarPeers,
playHaptic: true,
isLarge: updatedReactionIsLarge,
targetView: targetView,
addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in
guard let strongSelf = self, let chatDisplayNode = strongSelf.controllerInteraction.chatControllerNode() as? ChatControllerNode else {
return
}
chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
standaloneReactionAnimation.frame = chatDisplayNode.bounds
chatDisplayNode.addSubnode(standaloneReactionAnimation)
},
completion: { [weak standaloneReactionAnimation] in
standaloneReactionAnimation?.removeFromSupernode()
}
)
)
break
}
}
}
case let .custom(fileId):
if let itemFile = item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile {
reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: updatedReaction),
appearAnimation: itemFile,
stillAnimation: itemFile,
listAnimation: itemFile,
largeListAnimation: itemFile,
applicationAnimation: nil,
largeApplicationAnimation: nil,
isCustom: true
)
}
}
if let reactionItem = reactionItem, let targetView = itemNode.targetReactionView(value: updatedReaction) {
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: self.genericReactionEffect)
chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
var avatarPeers: [EnginePeer] = []
if item.message.id.peerId.namespace != Namespaces.Peer.CloudUser, let updateReactionPeer = updateReactionPeer {
avatarPeers = [updateReactionPeer]
}
chatDisplayNode.addSubnode(standaloneReactionAnimation)
standaloneReactionAnimation.frame = chatDisplayNode.bounds
standaloneReactionAnimation.animateReactionSelection(
context: self.context,
theme: item.presentationData.theme.theme,
animationCache: self.controllerInteraction.presentationContext.animationCache,
reaction: reactionItem,
avatarPeers: avatarPeers,
playHaptic: true,
isLarge: updatedReactionIsLarge,
targetView: targetView,
addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in
guard let strongSelf = self, let chatDisplayNode = strongSelf.controllerInteraction.chatControllerNode() as? ChatControllerNode else {
return
}
chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
standaloneReactionAnimation.frame = chatDisplayNode.bounds
chatDisplayNode.addSubnode(standaloneReactionAnimation)
},
completion: { [weak standaloneReactionAnimation] in
standaloneReactionAnimation?.removeFromSupernode()
}
)
}
}
return visibleNewIncomingReactionMessageIds