diff --git a/submodules/ReactionSelectionNode/BUILD b/submodules/ReactionSelectionNode/BUILD index 00e8cc7db3..8b6fbe3436 100644 --- a/submodules/ReactionSelectionNode/BUILD +++ b/submodules/ReactionSelectionNode/BUILD @@ -33,6 +33,7 @@ swift_library( "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/TextFormat:TextFormat", "//submodules/GZip:GZip", + "//submodules/ShimmerEffect:ShimmerEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 6b49c3f13e..3b316a198d 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -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 diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 0945f3ce3c..2ff7ca8fc1 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift index aff30b4df5..96aa671fe7 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index a3ef578c27..a83acc73b0 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1629,6 +1629,15 @@ func extractEmojiFileIds(message: StoreMessage, fileIds: inout Set) { 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 { 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 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) } } } diff --git a/submodules/TelegramCore/Sources/State/FetchChatList.swift b/submodules/TelegramCore/Sources/State/FetchChatList.swift index bc0d24b245..ff4a1dd8f3 100644 --- a/submodules/TelegramCore/Sources/State/FetchChatList.swift +++ b/submodules/TelegramCore/Sources/State/FetchChatList.swift @@ -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) } } } diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index 04ca364162..97f409dc68 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -43,13 +43,19 @@ enum FetchMessageHistoryHoleSource { } } -func resolveUnknownEmojiFiles(postbox: Postbox, source: FetchMessageHistoryHoleSource, messages: [StoreMessage], result: T) -> Signal { +func resolveUnknownEmojiFiles(postbox: Postbox, source: FetchMessageHistoryHoleSource, messages: [StoreMessage], reactions: [MessageReaction.Reaction], result: T) -> Signal { var fileIds = Set() 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(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 in return postbox.transaction { transaction -> T in return f(transaction, [], []) @@ -174,7 +180,7 @@ private func withResolvedAssociatedMessages(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 in return postbox.transaction { transaction -> T in return f(transaction, additionalPeers, additionalMessages) diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 1469dbc2a6..fe7e08a766 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -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) } } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 708eba20b0..5cc319cf34 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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)) diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index afd03a1ae6..d6c9a94fb3 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -451,6 +451,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let canReadHistory = Promise() 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() private var messageIdsWithReactionsScheduledForMarkAsSeen = Set() @@ -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