diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index da854711f0..6178722b8c 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -156,6 +156,7 @@ public protocol AnimatedStickerNode: ASDisplayNode { var completed: (Bool) -> Void { get set } var frameUpdated: (Int, Int) -> Void { get set } var currentFrameIndex: Int { get } + var currentFrameImage: UIImage? { get } var currentFrameCount: Int { get } var isPlaying: Bool { get } var stopAtNearestLoop: Bool { get set } @@ -173,6 +174,7 @@ public protocol AnimatedStickerNode: ASDisplayNode { func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode) func reset() func playOnce() + func playLoop() func play(firstFrame: Bool, fromIndex: Int?) func pause() func stop() @@ -225,6 +227,10 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke public var autoplay = false public var overrideVisibility: Bool = false + public var currentFrameImage: UIImage? { + return self.renderer?.renderer.currentFrameImage + } + public var visibility = false { didSet { if self.visibility != oldValue { @@ -421,6 +427,11 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke self.playbackMode = .once self.play() } + + public func playLoop() { + self.playbackMode = .loop + self.play() + } public func play(firstFrame: Bool = false, fromIndex: Int? = nil) { if !firstFrame { diff --git a/submodules/AnimatedStickerNode/Sources/AnimationRenderer.swift b/submodules/AnimatedStickerNode/Sources/AnimationRenderer.swift index e4e2264b15..26f3efe1f6 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimationRenderer.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimationRenderer.swift @@ -49,6 +49,8 @@ final class AnimationRendererPool { } protocol AnimationRenderer: ASDisplayNode { + var currentFrameImage: UIImage? { get } + func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void) func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) diff --git a/submodules/AnimatedStickerNode/Sources/CompressedAnimationRenderer.swift b/submodules/AnimatedStickerNode/Sources/CompressedAnimationRenderer.swift index 3446907812..4fe044fa76 100644 --- a/submodules/AnimatedStickerNode/Sources/CompressedAnimationRenderer.swift +++ b/submodules/AnimatedStickerNode/Sources/CompressedAnimationRenderer.swift @@ -37,6 +37,10 @@ final class CompressedAnimationRenderer: ASDisplayNode, AnimationRenderer { private let renderer: CompressedImageRenderer + var currentFrameImage: UIImage? { + return nil + } + override init() { self.renderer = CompressedImageRenderer(sharedContext: AnimationCompressor.SharedContext.shared)! diff --git a/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift index b5ca2678e6..38f1c5267f 100644 --- a/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift @@ -42,6 +42,13 @@ public final class DirectAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode } set(value) { } } + public var currentFrameImage: UIImage? { + if let contents = self.layer.contents { + return UIImage(cgImage: contents as! CGImage) + } else { + return nil + } + } public private(set) var isPlaying: Bool = false public var stopAtNearestLoop: Bool = false @@ -348,6 +355,9 @@ public final class DirectAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode public func playOnce() { } + public func playLoop() { + } + public func play(firstFrame: Bool, fromIndex: Int?) { if let fromIndex = fromIndex { self.frameIndex = fromIndex diff --git a/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift b/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift index 24bc3fa739..555c14dc46 100644 --- a/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift +++ b/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift @@ -10,6 +10,14 @@ final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer { private var highlightedContentNode: ASDisplayNode? private var highlightedColor: UIColor? private var highlightReplacesContent = false + + public var currentFrameImage: UIImage? { + if let contents = self.contents { + return UIImage(cgImage: contents as! CGImage) + } else { + return nil + } + } func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void) { assert(bytesPerRow > 0) diff --git a/submodules/Components/ReactionButtonListComponent/BUILD b/submodules/Components/ReactionButtonListComponent/BUILD index d2f35697e7..93fc5e91c1 100644 --- a/submodules/Components/ReactionButtonListComponent/BUILD +++ b/submodules/Components/ReactionButtonListComponent/BUILD @@ -20,6 +20,10 @@ swift_library( "//submodules/WebPBinding:WebPBinding", "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", "//submodules/Components/ReactionImageComponent:ReactionImageComponent", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 3c7a01a093..26b05c47c1 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -11,24 +11,169 @@ import UIKit import AnimatedAvatarSetNode import ReactionImageComponent import WebPBinding +import AnimationCache +import MultiAnimationRenderer +import EmojiTextAttachmentView +import TextFormat public final class ReactionIconView: PortalSourceView { - public let imageView: UIImageView + private var animationLayer: InlineStickerItemLayer? + + private var context: AccountContext? + private var fileId: Int64? + private var file: TelegramMediaFile? + private var animationCache: AnimationCache? + private var animationRenderer: MultiAnimationRenderer? + private var placeholderColor: UIColor? + private var size: CGSize? + private var animateIdle: Bool? + private var reaction: MessageReaction.Reaction? + + private var isAnimationHidden: Bool = false + + private var disposable: Disposable? override public init(frame: CGRect) { - self.imageView = UIImageView() - super.init(frame: frame) - - self.addSubview(self.imageView) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public func update(size: CGSize, transition: ContainedViewLayoutTransition) { - transition.updateFrame(view: self.imageView, frame: CGRect(origin: CGPoint(), size: size)) + deinit { + self.disposable?.dispose() + } + + public func update( + size: CGSize, + context: AccountContext, + file: TelegramMediaFile?, + fileId: Int64, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + placeholderColor: UIColor, + animateIdle: Bool, + reaction: MessageReaction.Reaction, + transition: ContainedViewLayoutTransition + ) { + self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer + self.placeholderColor = placeholderColor + self.size = size + self.animateIdle = animateIdle + self.reaction = reaction + + if self.fileId != fileId { + self.fileId = fileId + self.file = file + + self.animationLayer?.removeFromSuperlayer() + self.animationLayer = nil + + if let _ = file { + self.disposable?.dispose() + self.disposable = nil + + self.reloadFile() + } else { + self.disposable?.dispose() + + self.disposable = (context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + |> deliverOnMainQueue).start(next: { [weak self] files in + guard let strongSelf = self else { + return + } + strongSelf.file = files[fileId] + strongSelf.reloadFile() + }) + } + } + + if let animationLayer = self.animationLayer { + let iconSize: CGSize + switch reaction { + case .builtin: + iconSize = CGSize(width: floor(size.width * 2.0), height: floor(size.height * 2.0)) + case .custom: + iconSize = size + } + + transition.updateFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) + } + } + + public func updateIsAnimationHidden(isAnimationHidden: Bool, transition: ContainedViewLayoutTransition) { + if self.isAnimationHidden != isAnimationHidden { + self.isAnimationHidden = isAnimationHidden + + if let animationLayer = self.animationLayer { + transition.updateAlpha(layer: animationLayer, alpha: isAnimationHidden ? 0.0 : 1.0) + } + } + } + + private func reloadFile() { + guard let context = self.context, let file = self.file, let animationCache = self.animationCache, let animationRenderer = self.animationRenderer, let placeholderColor = self.placeholderColor, let size = self.size, let animateIdle = self.animateIdle, let reaction = self.reaction else { + return + } + + self.animationLayer?.removeFromSuperlayer() + self.animationLayer = nil + + let iconSize: CGSize + switch reaction { + case .builtin: + iconSize = CGSize(width: floor(size.width * 1.5), height: floor(size.height * 1.5)) + case .custom: + iconSize = size + } + + let animationLayer = InlineStickerItemLayer( + context: context, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute( + stickerPack: nil, + fileId: file.fileId.id, + file: file + ), + file: file, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: placeholderColor, + pointSize: CGSize(width: iconSize.width * 2.0, height: iconSize.height * 2.0) + ) + self.animationLayer = animationLayer + self.layer.addSublayer(animationLayer) + + animationLayer.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + + animationLayer.isVisibleForAnimations = animateIdle + } + + func reset() { + if let animationLayer = self.animationLayer { + self.animationLayer = nil + + animationLayer.removeFromSuperlayer() + } + if let disposable = self.disposable { + self.disposable = nil + disposable.dispose() + } + + self.context = nil + self.fileId = nil + self.file = nil + self.animationCache = nil + self.animationRenderer = nil + self.placeholderColor = nil + self.size = nil + self.animateIdle = nil + self.reaction = nil + + self.isAnimationHidden = false } } @@ -411,15 +556,13 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let spacing: CGFloat = 2.0 let boundingImageSize = CGSize(width: 20.0, height: 20.0) - let imageSize: CGSize - if let file = spec.component.reaction.centerAnimation { + let imageSize: CGSize = boundingImageSize + /*if let file = spec.component.reaction.centerAnimation { let defaultImageSize = CGSize(width: boundingImageSize.width + floor(boundingImageSize.width * 0.5 * 2.0), height: boundingImageSize.height + floor(boundingImageSize.height * 0.5 * 2.0)) imageSize = file.dimensions?.cgSize.aspectFitted(defaultImageSize) ?? defaultImageSize - } else if let file = spec.component.reaction.legacyIcon { - imageSize = file.dimensions?.cgSize.aspectFitted(boundingImageSize) ?? boundingImageSize } else { imageSize = boundingImageSize - } + }*/ var counterComponents: [String] = [] for character in countString(Int64(spec.component.count)) { @@ -435,7 +578,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } #endif*/ - let backgroundColor = spec.component.isSelected ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground + let backgroundColor = spec.component.chosenOrder != nil ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground let imageFrame = CGRect(origin: CGPoint(x: sideInsets + floorToScreenPixels((boundingImageSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize) @@ -467,11 +610,11 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } let backgroundColors = ReactionButtonAsyncNode.ContainerButtonNode.Colors( - background: spec.component.isSelected ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground, - foreground: spec.component.isSelected ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground, + background: spec.component.chosenOrder != nil ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground, + foreground: spec.component.chosenOrder != nil ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground, extractedBackground: spec.component.colors.extractedBackground, extractedForeground: spec.component.colors.extractedForeground, - isSelected: spec.component.isSelected + isSelected: spec.component.chosenOrder != nil ) var backgroundCounter: ReactionButtonAsyncNode.ContainerButtonNode.Counter? if let counterLayout = counterLayout { @@ -564,7 +707,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } func reset() { - self.iconView?.imageView.image = nil + self.iconView?.reset() self.layout = nil self.buttonNode.reset() @@ -577,7 +720,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { layout.spec.component.action(layout.spec.component.reaction.value) } - fileprivate func apply(layout: Layout, animation: ListViewItemUpdateAnimation) { + fileprivate func apply(layout: Layout, animation: ListViewItemUpdateAnimation, arguments: ReactionButtonsAsyncLayoutContainer.Arguments) { self.containerView.frame = CGRect(origin: CGPoint(), size: layout.size) self.containerView.contentView.frame = CGRect(origin: CGPoint(), size: layout.size) self.containerView.contentRect = CGRect(origin: CGPoint(), size: layout.size) @@ -587,10 +730,32 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { if let iconView = self.iconView { animation.animator.updateFrame(layer: iconView.layer, frame: layout.imageFrame, completion: nil) - iconView.update(size: layout.imageFrame.size, transition: animation.transition) - if self.layout?.spec.component.reaction != layout.spec.component.reaction { + if let fileId = layout.spec.component.reaction.animationFileId ?? layout.spec.component.reaction.centerAnimation?.fileId.id { + let animateIdle: Bool + if case .custom = layout.spec.component.reaction.value { + animateIdle = true + } else { + animateIdle = false + } + + iconView.update( + size: layout.imageFrame.size, + context: layout.spec.component.context, + file: layout.spec.component.reaction.centerAnimation, + fileId: fileId, + animationCache: arguments.animationCache, + animationRenderer: arguments.animationRenderer, + placeholderColor: .gray, + animateIdle: animateIdle, + reaction: layout.spec.component.reaction.value, + transition: animation.transition + ) + } + + /*if self.layout?.spec.component.reaction != layout.spec.component.reaction { if let file = layout.spec.component.reaction.centerAnimation { + if let image = ReactionImageCache.shared.get(reaction: layout.spec.component.reaction.value) { iconView.imageView.image = image } else { @@ -645,7 +810,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { strongSelf.iconView?.imageView.image = image })) } - } + }*/ } if !layout.spec.component.avatarPeers.isEmpty { @@ -683,7 +848,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { self.layout = layout } - public static func asyncLayout(_ item: ReactionNodePool.Item?) -> (ReactionButtonComponent) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionNodePool.Item) { + public static func asyncLayout(_ item: ReactionNodePool.Item?) -> (ReactionButtonComponent) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation, _ arguments: ReactionButtonsAsyncLayoutContainer.Arguments) -> ReactionNodePool.Item) { let currentLayout = item?.view.layout return { component in @@ -696,7 +861,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { layout = Layout.calculate(spec: spec, currentLayout: currentLayout) } - return (size: layout.size, apply: { animation in + return (size: layout.size, apply: { animation, arguments in var animation = animation let updatedItem: ReactionNodePool.Item if let item = item { @@ -706,7 +871,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { animation = .None } - updatedItem.view.apply(layout: layout, animation: animation) + updatedItem.view.apply(layout: layout, animation: animation, arguments: arguments) return updatedItem }) @@ -718,12 +883,12 @@ public final class ReactionButtonComponent: Equatable { public struct Reaction: Equatable { public var value: MessageReaction.Reaction public var centerAnimation: TelegramMediaFile? - public var legacyIcon: TelegramMediaFile? + public var animationFileId: Int64? - public init(value: MessageReaction.Reaction, centerAnimation: TelegramMediaFile?, legacyIcon: TelegramMediaFile?) { + public init(value: MessageReaction.Reaction, centerAnimation: TelegramMediaFile?, animationFileId: Int64?) { self.value = value self.centerAnimation = centerAnimation - self.legacyIcon = legacyIcon + self.animationFileId = animationFileId } public static func ==(lhs: Reaction, rhs: Reaction) -> Bool { @@ -733,7 +898,7 @@ public final class ReactionButtonComponent: Equatable { if lhs.centerAnimation?.fileId != rhs.centerAnimation?.fileId { return false } - if lhs.legacyIcon?.fileId != rhs.legacyIcon?.fileId { + if lhs.animationFileId != rhs.animationFileId { return false } return true @@ -770,7 +935,7 @@ public final class ReactionButtonComponent: Equatable { public let reaction: Reaction public let avatarPeers: [EnginePeer] public let count: Int - public let isSelected: Bool + public let chosenOrder: Int? public let action: (MessageReaction.Reaction) -> Void public init( @@ -779,7 +944,7 @@ public final class ReactionButtonComponent: Equatable { reaction: Reaction, avatarPeers: [EnginePeer], count: Int, - isSelected: Bool, + chosenOrder: Int?, action: @escaping (MessageReaction.Reaction) -> Void ) { self.context = context @@ -787,7 +952,7 @@ public final class ReactionButtonComponent: Equatable { self.reaction = reaction self.avatarPeers = avatarPeers self.count = count - self.isSelected = isSelected + self.chosenOrder = chosenOrder self.action = action } @@ -807,7 +972,7 @@ public final class ReactionButtonComponent: Equatable { if lhs.count != rhs.count { return false } - if lhs.isSelected != rhs.isSelected { + if lhs.chosenOrder != rhs.chosenOrder { return false } return true @@ -858,22 +1023,35 @@ public final class ReactionNodePool { } public final class ReactionButtonsAsyncLayoutContainer { + public final class Arguments { + public let animationCache: AnimationCache + public let animationRenderer: MultiAnimationRenderer + + public init( + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer + ) { + self.animationCache = animationCache + self.animationRenderer = animationRenderer + } + } + public struct Reaction { public var reaction: ReactionButtonComponent.Reaction public var count: Int public var peers: [EnginePeer] - public var isSelected: Bool + public var chosenOrder: Int? public init( reaction: ReactionButtonComponent.Reaction, count: Int, peers: [EnginePeer], - isSelected: Bool + chosenOrder: Int? ) { self.reaction = reaction self.count = count self.peers = peers - self.isSelected = isSelected + self.chosenOrder = chosenOrder } } @@ -883,7 +1061,7 @@ public final class ReactionButtonsAsyncLayoutContainer { } public var items: [Item] - public var apply: (ListViewItemUpdateAnimation) -> ApplyResult + public var apply: (ListViewItemUpdateAnimation, Arguments) -> ApplyResult } public struct ApplyResult { @@ -916,23 +1094,34 @@ public final class ReactionButtonsAsyncLayoutContainer { constrainedWidth: CGFloat ) -> Result { var items: [Result.Item] = [] - var applyItems: [(key: MessageReaction.Reaction, size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionNodePool.Item)] = [] + var applyItems: [(key: MessageReaction.Reaction, size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation, _ arguments: Arguments) -> ReactionNodePool.Item)] = [] var validIds = Set() for reaction in reactions.sorted(by: { lhs, rhs in var lhsCount = lhs.count - if lhs.isSelected { + if lhs.chosenOrder != nil { lhsCount -= 1 } var rhsCount = rhs.count - if rhs.isSelected { + if rhs.chosenOrder != nil { rhsCount -= 1 } if lhsCount != rhsCount { return lhsCount > rhsCount } + + if (lhs.chosenOrder != nil) != (rhs.chosenOrder != nil) { + if lhs.chosenOrder != nil { + return true + } else { + return false + } + } else if let lhsIndex = lhs.chosenOrder, let rhsIndex = rhs.chosenOrder { + return lhsIndex < rhsIndex + } + return false - }) { + }).prefix(10) { validIds.insert(reaction.reaction.value) var avatarPeers = reaction.peers @@ -952,7 +1141,7 @@ public final class ReactionButtonsAsyncLayoutContainer { reaction: reaction.reaction, avatarPeers: avatarPeers, count: reaction.count, - isSelected: reaction.isSelected, + chosenOrder: reaction.chosenOrder, action: action )) @@ -977,10 +1166,10 @@ public final class ReactionButtonsAsyncLayoutContainer { return Result( items: items, - apply: { animation in + apply: { animation, arguments in var items: [ApplyResult.Item] = [] for (key, size, apply) in applyItems { - let nodeItem = apply(animation) + let nodeItem = apply(animation, arguments) items.append(ApplyResult.Item(value: key, node: nodeItem, size: size)) if let current = self.buttons[key] { diff --git a/submodules/Components/ReactionListContextMenuContent/BUILD b/submodules/Components/ReactionListContextMenuContent/BUILD index feaf8c08f7..cd2e7552c1 100644 --- a/submodules/Components/ReactionListContextMenuContent/BUILD +++ b/submodules/Components/ReactionListContextMenuContent/BUILD @@ -22,6 +22,10 @@ swift_library( "//submodules/ContextUI:ContextUI", "//submodules/AvatarNode:AvatarNode", "//submodules/Components/ReactionImageComponent:ReactionImageComponent", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index d66b8916e1..3ce490102c 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -12,6 +12,10 @@ import AnimatedAvatarSetNode import ContextUI import AvatarNode import ReactionImageComponent +import AnimationCache +import MultiAnimationRenderer +import EmojiTextAttachmentView +import TextFormat private let avatarFont = avatarPlaceholderFont(size: 16.0) @@ -106,54 +110,113 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private final class ReactionTabListNode: ASDisplayNode { private final class ItemNode: ASDisplayNode { let context: AccountContext + let animationCache: AnimationCache + let animationRenderer: MultiAnimationRenderer let reaction: MessageReaction.Reaction? let count: Int let titleLabelNode: ImmediateTextNode - let iconNode: ASImageNode? - let reactionIconNode: ReactionImageNode? + var iconNode: ASImageNode? + var reactionLayer: InlineStickerItemLayer? + + private var iconFrame: CGRect? + private var file: TelegramMediaFile? + private var fileDisposable: Disposable? private var theme: PresentationTheme? var action: ((MessageReaction.Reaction?) -> Void)? - init(context: AccountContext, availableReactions: AvailableReactions?, reaction: MessageReaction.Reaction?, count: Int) { + init(context: AccountContext, availableReactions: AvailableReactions?, reaction: MessageReaction.Reaction?, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, count: Int) { self.context = context self.reaction = reaction self.count = count + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.titleLabelNode = ImmediateTextNode() self.titleLabelNode.isUserInteractionEnabled = false - if let reaction = reaction { - self.reactionIconNode = ReactionImageNode(context: context, availableReactions: availableReactions, reaction: reaction, displayPixelSize: CGSize(width: 30.0 * UIScreenScale, height: 30.0 * UIScreenScale)) - self.reactionIconNode?.isUserInteractionEnabled = false - self.iconNode = nil - } else { - self.reactionIconNode = nil - self.iconNode = ASImageNode() - self.iconNode?.isUserInteractionEnabled = false - } - super.init() self.addSubnode(self.titleLabelNode) - if let iconNode = self.iconNode { + + if let reaction = reaction { + switch reaction { + case .builtin: + if let availableReactions = availableReactions { + for availableReaction in availableReactions.reactions { + if availableReaction.value == reaction { + self.file = availableReaction.centerAnimation + self.updateReactionLayer() + break + } + } + } + case let .custom(fileId): + self.fileDisposable = (context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + |> deliverOnMainQueue).start(next: { [weak self] files in + guard let strongSelf = self, let file = files[fileId] else { + return + } + strongSelf.file = file + strongSelf.updateReactionLayer() + }) + } + } else { + let iconNode = ASImageNode() + self.iconNode = iconNode self.addSubnode(iconNode) } - if let reactionIconNode = self.reactionIconNode { - self.addSubnode(reactionIconNode) - } self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } + deinit { + self.fileDisposable?.dispose() + } + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.action?(self.reaction) } } + private func updateReactionLayer() { + guard let file = self.file else { + return + } + + if let reactionLayer = self.reactionLayer { + self.reactionLayer = nil + reactionLayer.removeFromSuperlayer() + } + + let reactionLayer = InlineStickerItemLayer( + context: context, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: file.fileId.id, file: file), + file: file, + cache: self.animationCache, + renderer: self.animationRenderer, + placeholderColor: UIColor(white: 0.0, alpha: 0.1), + pointSize: CGSize(width: 50.0, height: 50.0) + ) + self.reactionLayer = reactionLayer + + if let reaction = self.reaction, case .custom = reaction { + reactionLayer.isVisibleForAnimations = true + } + self.layer.addSublayer(reactionLayer) + + if var iconFrame = self.iconFrame { + if let reaction = self.reaction, case .builtin = reaction { + iconFrame = iconFrame.insetBy(dx: -iconFrame.width * 0.5, dy: -iconFrame.height * 0.5) + } + reactionLayer.frame = iconFrame + } + } + func update(presentationData: PresentationData, constrainedSize: CGSize, isSelected: Bool) -> CGSize { if presentationData.theme !== self.theme { self.theme = presentationData.theme @@ -166,11 +229,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let sideInset: CGFloat = 12.0 let iconSpacing: CGFloat = 4.0 - var iconSize = CGSize(width: 22.0, height: 22.0) - if let _ = self.reactionIconNode { - } else if let iconNode = self.iconNode, let image = iconNode.image { - iconSize = image.size.aspectFitted(iconSize) - } + + let iconSize = CGSize(width: 22.0, height: 22.0) + self.iconFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) self.titleLabelNode.attributedText = NSAttributedString(string: "\(count)", font: Font.medium(11.0), textColor: presentationData.theme.contextMenu.primaryColor) let titleSize = self.titleLabelNode.updateLayout(constrainedSize) @@ -179,13 +240,17 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + iconSpacing, y: floorToScreenPixels((constrainedSize.height - titleSize.height) / 2.0)), size: titleSize) - if let reactionIconNode = self.reactionIconNode { - reactionIconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) - reactionIconNode.update(size: iconSize) - } else if let iconNode = self.iconNode { + if let iconNode = self.iconNode { iconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) } + if let reactionLayer = self.reactionLayer, var iconFrame = self.iconFrame { + if let reaction = self.reaction, case .builtin = reaction { + iconFrame = iconFrame.insetBy(dx: -iconFrame.width * 0.5, dy: -iconFrame.height * 0.5) + } + reactionLayer.frame = iconFrame + } + return CGSize(width: contentSize.width, height: constrainedSize.height) } } @@ -201,7 +266,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent var action: ((MessageReaction.Reaction?) -> Void)? - init(context: AccountContext, availableReactions: AvailableReactions?, reactions: [(MessageReaction.Reaction?, Int)], message: EngineMessage) { + init(context: AccountContext, availableReactions: AvailableReactions?, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, reactions: [(MessageReaction.Reaction?, Int)], message: EngineMessage) { self.scrollNode = ASScrollNode() self.scrollNode.canCancelAllTouchesInViews = true self.scrollNode.view.delaysContentTouches = false @@ -213,7 +278,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true self.itemNodes = reactions.map { reaction, count in - return ItemNode(context: context, availableReactions: availableReactions, reaction: reaction, count: count) + return ItemNode(context: context, availableReactions: availableReactions, reaction: reaction, animationCache: animationCache, animationRenderer: animationRenderer, count: count) } self.selectionHighlightNode = ASDisplayNode() @@ -287,20 +352,29 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private final class ItemNode: HighlightTrackingButtonNode { let context: AccountContext let availableReactions: AvailableReactions? + let animationCache: AnimationCache + let animationRenderer: MultiAnimationRenderer let highlightBackgroundNode: ASDisplayNode let avatarNode: AvatarNode let titleLabelNode: ImmediateTextNode var credibilityIconNode: ASImageNode? let separatorNode: ASDisplayNode - var reactionIconNode: ReactionImageNode? + + private var reactionLayer: InlineStickerItemLayer? + private var iconFrame: CGRect? + private var file: TelegramMediaFile? + private var fileDisposable: Disposable? + let action: () -> Void private var item: EngineMessageReactionListContext.Item? - init(context: AccountContext, availableReactions: AvailableReactions?, action: @escaping () -> Void) { + init(context: AccountContext, availableReactions: AvailableReactions?, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, action: @escaping () -> Void) { self.action = action self.context = context self.availableReactions = availableReactions + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.isAccessibilityElement = false @@ -342,10 +416,48 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) } + deinit { + self.fileDisposable?.dispose() + } + @objc private func pressed() { self.action() } + private func updateReactionLayer() { + guard let file = self.file else { + return + } + + if let reactionLayer = self.reactionLayer { + self.reactionLayer = nil + reactionLayer.removeFromSuperlayer() + } + + let reactionLayer = InlineStickerItemLayer( + context: context, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: file.fileId.id, file: file), + file: file, + cache: self.animationCache, + renderer: self.animationRenderer, + placeholderColor: UIColor(white: 0.0, alpha: 0.1), + pointSize: CGSize(width: 50.0, height: 50.0) + ) + self.reactionLayer = reactionLayer + if let item = self.item, let reaction = item.reaction, case .custom = reaction { + reactionLayer.isVisibleForAnimations = true + } + self.layer.addSublayer(reactionLayer) + + if var iconFrame = self.iconFrame { + if let item = self.item, let reaction = item.reaction, case .builtin = reaction { + iconFrame = iconFrame.insetBy(dx: -iconFrame.width * 0.5, dy: -iconFrame.height * 0.5) + } + reactionLayer.frame = iconFrame + } + } + func update(size: CGSize, presentationData: PresentationData, item: EngineMessageReactionListContext.Item, isLast: Bool, syncronousLoad: Bool) { let avatarInset: CGFloat = 12.0 let avatarSpacing: CGFloat = 8.0 @@ -353,14 +465,40 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let sideInset: CGFloat = 16.0 let reaction: MessageReaction.Reaction? = item.reaction - if let reaction = reaction { - if self.reactionIconNode == nil { - let reactionIconNode = ReactionImageNode(context: self.context, availableReactions: self.availableReactions, reaction: reaction, displayPixelSize: CGSize(width: 30.0 * UIScreenScale, height: 30.0 * UIScreenScale)) - self.reactionIconNode = reactionIconNode - self.addSubnode(reactionIconNode) + + if reaction != self.item?.reaction { + if let reaction = reaction { + switch reaction { + case .builtin: + if let availableReactions = self.availableReactions { + for availableReaction in availableReactions.reactions { + if availableReaction.value == reaction { + self.file = availableReaction.centerAnimation + self.updateReactionLayer() + break + } + } + } + case let .custom(fileId): + self.fileDisposable = (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + |> deliverOnMainQueue).start(next: { [weak self] files in + guard let strongSelf = self, let file = files[fileId] else { + return + } + strongSelf.file = file + strongSelf.updateReactionLayer() + }) + } + } else { + self.file = nil + self.fileDisposable?.dispose() + self.fileDisposable = nil + + if let reactionLayer = self.reactionLayer { + self.reactionLayer = nil + reactionLayer.removeFromSuperlayer() + } } - } else if let reactionIconNode = self.reactionIconNode { - reactionIconNode.removeFromSupernode() } if self.item != item { @@ -407,7 +545,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.titleLabelNode.attributedText = NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor) var maxTextWidth: CGFloat = size.width - avatarInset - avatarSize - avatarSpacing - sideInset - additionalTitleInset - if reactionIconNode != nil { + if reaction != nil { maxTextWidth -= 32.0 } let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 100.0)) @@ -433,10 +571,14 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent credibilityIconNode.removeFromSupernode() } - if let reactionIconNode = self.reactionIconNode { - let reactionSize = CGSize(width: 22.0, height: 22.0) - reactionIconNode.frame = CGRect(origin: CGPoint(x: size.width - 32.0 - floor((32.0 - reactionSize.width) / 2.0), y: floor((size.height - reactionSize.height) / 2.0)), size: reactionSize) - reactionIconNode.update(size: reactionSize) + let reactionSize = CGSize(width: 22.0, height: 22.0) + self.iconFrame = CGRect(origin: CGPoint(x: size.width - 32.0 - floor((32.0 - reactionSize.width) / 2.0), y: floor((size.height - reactionSize.height) / 2.0)), size: reactionSize) + + if let reactionLayer = self.reactionLayer, var iconFrame = self.iconFrame { + if let reaction = reaction, case .builtin = reaction { + iconFrame = iconFrame.insetBy(dx: -iconFrame.width * 0.5, dy: -iconFrame.height * 0.5) + } + reactionLayer.frame = iconFrame } self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: UIScreenPixel)) @@ -464,6 +606,11 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } } + + /*for _ in 0 ..< 5 { + mergedItems.append(contentsOf: mergedItems) + }*/ + self.mergedItems = mergedItems } @@ -499,6 +646,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private let context: AccountContext private let availableReactions: AvailableReactions? + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer let reaction: MessageReaction.Reaction? private let requestUpdate: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void private let requestUpdateApparentHeight: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void @@ -526,6 +675,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent init( context: AccountContext, availableReactions: AvailableReactions?, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, message: EngineMessage, reaction: MessageReaction.Reaction?, readStats: MessageReadStats?, @@ -535,6 +686,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent ) { self.context = context self.availableReactions = availableReactions + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.reaction = reaction self.requestUpdate = requestUpdate self.requestUpdateApparentHeight = requestUpdateApparentHeight @@ -631,7 +784,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } else { let openPeer = self.openPeer let peerId = item.peer.id - itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: { + itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, animationCache: self.animationCache, animationRenderer: self.animationRenderer, action: { openPeer(peerId) }) self.itemNodes[index] = itemNode @@ -763,6 +916,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent final class ItemsNode: ASDisplayNode, ContextControllerItemsNode, UIGestureRecognizerDelegate { private let context: AccountContext private let availableReactions: AvailableReactions? + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let message: EngineMessage private let readStats: MessageReadStats? private let reactions: [(MessageReaction.Reaction?, Int)] @@ -791,6 +946,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent init( context: AccountContext, availableReactions: AvailableReactions?, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, message: EngineMessage, reaction: MessageReaction.Reaction?, readStats: MessageReadStats?, @@ -801,6 +958,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent ) { self.context = context self.availableReactions = availableReactions + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.message = message self.readStats = readStats self.openPeer = openPeer @@ -834,7 +993,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } if reactions.count > 2 && totalCount > 10 { - self.tabListNode = ReactionTabListNode(context: context, availableReactions: availableReactions, reactions: reactions, message: message) + self.tabListNode = ReactionTabListNode(context: context, availableReactions: availableReactions, animationCache: animationCache, animationRenderer: animationRenderer, reactions: reactions, message: message) } self.reactions = reactions @@ -1021,6 +1180,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent tabNode = ReactionsTabNode( context: self.context, availableReactions: self.availableReactions, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, message: self.message, reaction: self.reactions[index].0, readStats: self.reactions[index].0 == nil ? self.readStats : nil, @@ -1134,6 +1295,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let context: AccountContext let availableReactions: AvailableReactions? + let animationCache: AnimationCache + let animationRenderer: MultiAnimationRenderer let message: EngineMessage let reaction: MessageReaction.Reaction? let readStats: MessageReadStats? @@ -1143,6 +1306,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent public init( context: AccountContext, availableReactions: AvailableReactions?, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, message: EngineMessage, reaction: MessageReaction.Reaction?, readStats: MessageReadStats?, @@ -1151,6 +1316,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent ) { self.context = context self.availableReactions = availableReactions + self.animationCache = animationCache + self.animationRenderer = animationRenderer self.message = message self.reaction = reaction self.readStats = readStats @@ -1165,6 +1332,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent return ItemsNode( context: self.context, availableReactions: self.availableReactions, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, message: self.message, reaction: self.reaction, readStats: self.readStats, diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index ea9bf5a775..965c961163 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -8,6 +8,7 @@ import Markdown import AppBundle import TextFormat import TextNodeWithEntities +import SwiftSignalKit private final class ContextActionsSelectionGestureRecognizer: UIPanGestureRecognizer { var updateLocation: ((CGPoint, Bool) -> Void)? @@ -351,6 +352,8 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { private let iconNode: ASImageNode private let placeholderNode: ASDisplayNode + var tip: ContextController.Tip + private let text: String private var arguments: TextNodeWithEntities.Arguments? private var file: TelegramMediaFile? @@ -362,6 +365,7 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { var requestDismiss: (@escaping () -> Void) -> Void = { _ in } init(presentationData: PresentationData, tip: ContextController.Tip) { + self.tip = tip self.presentationData = presentationData self.highlightBackgroundNode = ASDisplayNode() @@ -645,14 +649,19 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { } final class ContextActionsContainerNode: ASDisplayNode { + private let presentationData: PresentationData + private let getController: () -> ContextControllerProtocol? private let blurBackground: Bool private let shadowNode: ASImageNode private let additionalShadowNode: ASImageNode? private let additionalActionsNode: InnerActionsContainerNode? private let actionsNode: InnerActionsContainerNode - private let textSelectionTipNode: InnerTextSelectionTipContainerNode? private let scrollNode: ASScrollNode + private var tip: ContextController.Tip? + private var textSelectionTipNode: InnerTextSelectionTipContainerNode? + private var textSelectionTipNodeDisposable: Disposable? + var panSelectionGestureEnabled: Bool = true { didSet { if self.panSelectionGestureEnabled != oldValue { @@ -666,6 +675,8 @@ final class ContextActionsContainerNode: ASDisplayNode { } init(presentationData: PresentationData, items: ContextController.Items, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, requestLayout: @escaping () -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) { + self.presentationData = presentationData + self.getController = getController self.blurBackground = blurBackground self.shadowNode = ASImageNode() self.shadowNode.displaysAsynchronously = false @@ -698,15 +709,8 @@ final class ContextActionsContainerNode: ASDisplayNode { } self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: itemList, getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, feedbackTap: feedbackTap, blurBackground: blurBackground) - if let tip = items.tip { - let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData, tip: tip) - textSelectionTipNode.requestDismiss = { completion in - getController()?.dismiss(completion: completion) - } - self.textSelectionTipNode = textSelectionTipNode - } else { - self.textSelectionTipNode = nil - } + + self.tip = items.tip self.scrollNode = ASScrollNode() self.scrollNode.canCancelAllTouchesInViews = true @@ -722,8 +726,23 @@ final class ContextActionsContainerNode: ASDisplayNode { self.additionalShadowNode.flatMap(self.addSubnode) self.additionalActionsNode.flatMap(self.scrollNode.addSubnode) self.scrollNode.addSubnode(self.actionsNode) - self.textSelectionTipNode.flatMap(self.scrollNode.addSubnode) self.addSubnode(self.scrollNode) + + if let tipSignal = items.tipSignal { + self.textSelectionTipNodeDisposable = (tipSignal + |> deliverOnMainQueue).start(next: { [weak self] tip in + guard let strongSelf = self else { + return + } + + strongSelf.tip = tip + requestLayout() + }) + } + } + + deinit { + self.textSelectionTipNodeDisposable?.dispose() } func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { @@ -756,6 +775,29 @@ final class ContextActionsContainerNode: ASDisplayNode { transition.updateFrame(node: self.actionsNode, frame: bounds) + if let tip = self.tip { + if let textSelectionTipNode = self.textSelectionTipNode, textSelectionTipNode.tip == tip { + } else { + if let textSelectionTipNode = self.textSelectionTipNode { + self.textSelectionTipNode = nil + textSelectionTipNode.removeFromSupernode() + } + + let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: self.presentationData, tip: tip) + let getController = self.getController + textSelectionTipNode.requestDismiss = { completion in + getController()?.dismiss(completion: completion) + } + self.textSelectionTipNode = textSelectionTipNode + self.scrollNode.addSubnode(textSelectionTipNode) + } + } else { + if let textSelectionTipNode = self.textSelectionTipNode { + self.textSelectionTipNode = nil + textSelectionTipNode.removeFromSupernode() + } + } + if let textSelectionTipNode = self.textSelectionTipNode { contentSize.height += 8.0 let textSelectionTipSize = textSelectionTipNode.updateLayout(widthClass: widthClass, width: actionsSize.width, transition: transition) diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 36e95b5822..cd85bff8bc 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -10,6 +10,8 @@ import SwiftSignalKit import AccountContext import TextNodeWithEntities import EntityKeyboard +import AnimationCache +import MultiAnimationRenderer private let animationDurationFactor: Double = 1.0 @@ -1518,8 +1520,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi }) } - if !items.reactionItems.isEmpty, let context = items.context { - let reactionContextNode = ReactionContextNode(context: context, presentationData: self.presentationData, items: items.reactionItems, getEmojiContent: items.getEmojiContent, isExpandedUpdated: { _ in }) + if !items.reactionItems.isEmpty, let context = items.context, let animationCache = items.animationCache { + let reactionContextNode = ReactionContextNode(context: context, animationCache: animationCache, presentationData: self.presentationData, items: items.reactionItems, getEmojiContent: items.getEmojiContent, isExpandedUpdated: { _ in }) self.reactionContextNode = reactionContextNode self.addSubnode(reactionContextNode) @@ -2380,17 +2382,21 @@ public final class ContextController: ViewController, StandalonePresentableContr public var content: Content public var context: AccountContext? public var reactionItems: [ReactionContextItem] - public var getEmojiContent: (() -> Signal)? + public var animationCache: AnimationCache? + public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? public var disablePositionLock: Bool public var tip: Tip? + public var tipSignal: Signal? - public init(content: Content, context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], getEmojiContent: (() -> Signal)? = nil, disablePositionLock: Bool = false, tip: Tip? = nil) { + public init(content: Content, context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], animationCache: AnimationCache? = nil, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? = nil, disablePositionLock: Bool = false, tip: Tip? = nil, tipSignal: Signal? = nil) { self.content = content self.context = context + self.animationCache = animationCache self.reactionItems = reactionItems self.getEmojiContent = getEmojiContent self.disablePositionLock = disablePositionLock self.tip = tip + self.tipSignal = tipSignal } public init() { @@ -2400,6 +2406,7 @@ public final class ContextController: ViewController, StandalonePresentableContr self.getEmojiContent = nil self.disablePositionLock = false self.tip = nil + self.tipSignal = nil } } @@ -2408,11 +2415,46 @@ public final class ContextController: ViewController, StandalonePresentableContr case slide(forward: Bool) } - public enum Tip { + public enum Tip: Equatable { case textSelection case messageViewsPrivacy case messageCopyProtection(isChannel: Bool) case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?) + + public static func ==(lhs: Tip, rhs: Tip) -> Bool { + switch lhs { + case .textSelection: + if case .textSelection = rhs { + return true + } else { + return false + } + case .messageViewsPrivacy: + if case .messageViewsPrivacy = rhs { + return true + } else { + return false + } + case let .messageCopyProtection(isChannel): + if case .messageCopyProtection(isChannel) = rhs { + return true + } else { + return false + } + case let .animatedEmoji(text, _, file, _): + if case let .animatedEmoji(rhsText, _, rhsFile, _) = rhs { + if text != rhsText { + return false + } + if file?.fileId != rhsFile?.fileId { + return false + } + return true + } else { + return false + } + } + } } public final class ActionsHeight { diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 5eb73fd7c3..538e4ea2cf 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -10,6 +10,8 @@ import AccountContext import ReactionSelectionNode import Markdown import EntityKeyboard +import AnimationCache +import MultiAnimationRenderer public protocol ContextControllerActionsStackItemNode: ASDisplayNode { func update( @@ -36,7 +38,8 @@ public protocol ContextControllerActionsStackItem: AnyObject { ) -> ContextControllerActionsStackItemNode var tip: ContextController.Tip? { get } - var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], (() -> Signal)?)? { get } + var tipSignal: Signal? { get } + var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { get } } protocol ContextControllerActionsListItemNode: ASDisplayNode { @@ -620,17 +623,20 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack } private let items: [ContextMenuItem] - let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], (() -> Signal)?)? + let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? let tip: ContextController.Tip? + let tipSignal: Signal? init( items: [ContextMenuItem], - reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], (() -> Signal)?)?, - tip: ContextController.Tip? + reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, + tip: ContextController.Tip?, + tipSignal: Signal? ) { self.items = items self.reactionItems = reactionItems self.tip = tip + self.tipSignal = tipSignal } func node( @@ -704,17 +710,20 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta } private let content: ContextControllerItemsContent - let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], (() -> Signal)?)? + let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? let tip: ContextController.Tip? + let tipSignal: Signal? init( content: ContextControllerItemsContent, - reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], (() -> Signal)?)?, - tip: ContextController.Tip? + reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, + tip: ContextController.Tip?, + tipSignal: Signal? ) { self.content = content self.reactionItems = reactionItems self.tip = tip + self.tipSignal = tipSignal } func node( @@ -733,15 +742,15 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta } func makeContextControllerActionsStackItem(items: ContextController.Items) -> ContextControllerActionsStackItem { - var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], (() -> Signal)?)? - if let context = items.context, !items.reactionItems.isEmpty { - reactionItems = (context, items.reactionItems, items.getEmojiContent) + var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? + if let context = items.context, let animationCache = items.animationCache, !items.reactionItems.isEmpty { + reactionItems = (context, items.reactionItems, animationCache, items.getEmojiContent) } switch items.content { case let .list(listItems): - return ContextControllerActionsListStackItem(items: listItems, reactionItems: reactionItems, tip: items.tip) + return ContextControllerActionsListStackItem(items: listItems, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal) case let .custom(customContent): - return ContextControllerActionsCustomStackItem(content: customContent, reactionItems: reactionItems, tip: items.tip) + return ContextControllerActionsCustomStackItem(content: customContent, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal) } } @@ -849,12 +858,15 @@ final class ContextControllerActionsStackNode: ASDisplayNode { let requestUpdate: (ContainedViewLayoutTransition) -> Void let node: ContextControllerActionsStackItemNode let dimNode: ASDisplayNode - let tip: ContextController.Tip? + var tip: ContextController.Tip? + let tipSignal: Signal? var tipNode: InnerTextSelectionTipContainerNode? - let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], (() -> Signal)?)? + let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? var storedScrollingState: CGFloat? let positionLock: CGFloat? + private var tipDisposable: Disposable? + init( getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, @@ -862,7 +874,8 @@ final class ContextControllerActionsStackNode: ASDisplayNode { requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void, item: ContextControllerActionsStackItem, tip: ContextController.Tip?, - reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], (() -> Signal)?)?, + tipSignal: Signal?, + reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, positionLock: CGFloat? ) { self.getController = getController @@ -882,6 +895,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.positionLock = positionLock self.tip = tip + self.tipSignal = tipSignal super.init() @@ -889,6 +903,21 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.addSubnode(self.node) self.addSubnode(self.dimNode) + + if let tipSignal = tipSignal { + self.tipDisposable = (tipSignal + |> deliverOnMainQueue).start(next: { [weak self] tip in + guard let strongSelf = self else { + return + } + strongSelf.tip = tip + requestUpdate(.immediate) + }) + } + } + + deinit { + self.tipDisposable?.dispose() } func update( @@ -922,7 +951,12 @@ final class ContextControllerActionsStackNode: ASDisplayNode { func updateTip(presentationData: PresentationData, width: CGFloat, transition: ContainedViewLayoutTransition) -> (node: ASDisplayNode, height: CGFloat)? { if let tip = self.tip { var updatedTransition = transition - if self.tipNode == nil { + if let tipNode = self.tipNode, tipNode.tip == tip { + } else { + if let tipNode = self.tipNode { + self.tipNode = nil + tipNode.removeFromSupernode() + } updatedTransition = .immediate let tipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData, tip: tip) tipNode.requestDismiss = { [weak self] completion in @@ -983,7 +1017,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { private var selectionPanGesture: UIPanGestureRecognizer? - var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], getEmojiContent: (() -> Signal)?)? { + var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { return self.itemContainers.last?.reactionItems } @@ -1076,6 +1110,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { }, item: item, tip: item.tip, + tipSignal: item.tipSignal, reactionItems: item.reactionItems, positionLock: positionLock ) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index a1e94e99fe..859d8b0dad 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -441,6 +441,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } else { reactionContextNode = ReactionContextNode( context: reactionItems.context, + animationCache: reactionItems.animationCache, presentationData: presentationData, items: reactionItems.reactionItems, getEmojiContent: reactionItems.getEmojiContent, diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/ContextUI/Sources/PeekControllerNode.swift index af9503a098..0ffc075cc9 100644 --- a/submodules/ContextUI/Sources/PeekControllerNode.swift +++ b/submodules/ContextUI/Sources/PeekControllerNode.swift @@ -78,7 +78,7 @@ final class PeekControllerNode: ViewControllerTracingNode { var feedbackTapImpl: (() -> Void)? var activatedActionImpl: (() -> Void)? var requestLayoutImpl: (() -> Void)? - self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(content: .list(content.menuItems())), getController: { [weak controller] in + self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(content: .list(content.menuItems()), animationCache: nil), getController: { [weak controller] in return controller }, actionSelected: { result in activatedActionImpl?() @@ -377,7 +377,7 @@ final class PeekControllerNode: ViewControllerTracingNode { self.contentNodeHasValidLayout = false let previousActionsContainerNode = self.actionsContainerNode - self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: ContextController.Items(content: .list(content.menuItems())), getController: { [weak self] in + self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: ContextController.Items(content: .list(content.menuItems()), animationCache: nil), getController: { [weak self] in return self?.controller }, actionSelected: { [weak self] result in self?.requestDismiss() diff --git a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift index 6776bd02b4..7de6a36d20 100644 --- a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift +++ b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift @@ -12,18 +12,24 @@ import PresentationDataUtils import AccountContext import PresentationDataUtils +private enum PeerReactionsMode { + case all + case some + case empty +} + private final class PeerAllowedReactionListControllerArguments { let context: AccountContext - let toggleAll: () -> Void + let setMode: (PeerReactionsMode) -> Void let toggleItem: (MessageReaction.Reaction) -> Void init( context: AccountContext, - toggleAll: @escaping () -> Void, + setMode: @escaping (PeerReactionsMode) -> Void, toggleItem: @escaping (MessageReaction.Reaction) -> Void ) { self.context = context - self.toggleAll = toggleAll + self.setMode = setMode self.toggleItem = toggleItem } } @@ -35,13 +41,19 @@ private enum PeerAllowedReactionListControllerSection: Int32 { private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { enum StableId: Hashable { + case allowAllHeader case allowAll + case allowSome + case allowNone case allowAllInfo case itemsHeader case item(MessageReaction.Reaction) } + case allowAllHeader(String) case allowAll(text: String, isEnabled: Bool) + case allowSome(text: String, isEnabled: Bool) + case allowNone(text: String, isEnabled: Bool) case allowAllInfo(String) case itemsHeader(String) @@ -49,7 +61,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .allowAll, .allowAllInfo: + case .allowAllHeader, .allowAll, .allowSome, .allowNone, .allowAllInfo: return PeerAllowedReactionListControllerSection.all.rawValue case .itemsHeader, .item: return PeerAllowedReactionListControllerSection.items.rawValue @@ -58,8 +70,14 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { var stableId: StableId { switch self { + case .allowAllHeader: + return .allowAllHeader case .allowAll: return .allowAll + case .allowSome: + return .allowSome + case .allowNone: + return .allowNone case .allowAllInfo: return .allowAllInfo case .itemsHeader: @@ -71,12 +89,18 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { var sortId: Int { switch self { - case .allowAll: + case .allowAllHeader: return 0 - case .allowAllInfo: + case .allowAll: return 1 - case .itemsHeader: + case .allowSome: return 2 + case .allowNone: + return 3 + case .allowAllInfo: + return 4 + case .itemsHeader: + return 5 case let .item(index, _, _, _, _, _): return 100 + index } @@ -84,12 +108,30 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { static func ==(lhs: PeerAllowedReactionListControllerEntry, rhs: PeerAllowedReactionListControllerEntry) -> Bool { switch lhs { + case let .allowAllHeader(text): + if case .allowAllHeader(text) = rhs { + return true + } else { + return false + } case let .allowAll(text, isEnabled): if case .allowAll(text, isEnabled) = rhs { return true } else { return false } + case let .allowSome(text, isEnabled): + if case .allowSome(text, isEnabled) = rhs { + return true + } else { + return false + } + case let .allowNone(text, isEnabled): + if case .allowNone(text, isEnabled) = rhs { + return true + } else { + return false + } case let .allowAllInfo(text): if case .allowAllInfo(text) = rhs { return true @@ -118,10 +160,65 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! PeerAllowedReactionListControllerArguments switch self { + case let .allowAllHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .allowAll(text, isEnabled): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: isEnabled, sectionId: self.section, style: .blocks, updated: { _ in - arguments.toggleAll() - }) + return ItemListCheckboxItem( + presentationData: presentationData, + icon: nil, + iconSize: nil, + iconPlacement: .default, + title: text, + subtitle: nil, + style: .right, + color: .accent, + textColor: .primary, + checked: isEnabled, + zeroSeparatorInsets: false, + sectionId: self.section, + action: { + arguments.setMode(.all) + }, + deleteAction: nil + ) + case let .allowSome(text, isEnabled): + return ItemListCheckboxItem( + presentationData: presentationData, + icon: nil, + iconSize: nil, + iconPlacement: .default, + title: text, + subtitle: nil, + style: .right, + color: .accent, + textColor: .primary, + checked: isEnabled, + zeroSeparatorInsets: false, + sectionId: self.section, + action: { + arguments.setMode(.some) + }, + deleteAction: nil + ) + case let .allowNone(text, isEnabled): + return ItemListCheckboxItem( + presentationData: presentationData, + icon: nil, + iconSize: nil, + iconPlacement: .default, + title: text, + subtitle: nil, + style: .right, + color: .accent, + textColor: .primary, + checked: isEnabled, + zeroSeparatorInsets: false, + sectionId: self.section, + action: { + arguments.setMode(.empty) + }, + deleteAction: nil + ) case let .allowAllInfo(text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .itemsHeader(text): @@ -145,6 +242,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { } private struct PeerAllowedReactionListControllerState: Equatable { + var updatedMode: PeerReactionsMode? var updatedAllowedReactions: Set? = nil } @@ -157,24 +255,48 @@ private func peerAllowedReactionListControllerEntries( ) -> [PeerAllowedReactionListControllerEntry] { var entries: [PeerAllowedReactionListControllerEntry] = [] - if let availableReactions = availableReactions, let allowedReactions = state.updatedAllowedReactions { - entries.append(.allowAll(text: presentationData.strings.PeerInfo_AllowedReactions_AllowAllText, isEnabled: !allowedReactions.isEmpty)) + if let availableReactions = availableReactions, let allowedReactions = state.updatedAllowedReactions, let mode = state.updatedMode { + //TODO:localize + entries.append(.allowAllHeader("AVAILABLE REACTIONS")) + + //TODO:localize + entries.append(.allowAll(text: "All Reactions", isEnabled: mode == .all)) + entries.append(.allowSome(text: "Some Reactions", isEnabled: mode == .some)) + entries.append(.allowNone(text: "No Reactions", isEnabled: mode == .empty)) + let allInfoText: String if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - allInfoText = presentationData.strings.PeerInfo_AllowedReactions_AllowAllChannelInfo + switch mode { + case .all: + allInfoText = "Subscribers of this channel can use any emoji as reactions to messages." + case .some: + allInfoText = "You can select emoji that will allow subscribers of your channel to react to messages." + case .empty: + allInfoText = "Subscribers of the channel can't add any reactions to messages." + } } else { - allInfoText = presentationData.strings.PeerInfo_AllowedReactions_AllowAllGroupInfo + switch mode { + case .all: + allInfoText = "Members of this group can use any emoji as reactions to messages." + case .some: + allInfoText = "You can select emoji that will allow members of your group to react to messages." + case .empty: + allInfoText = "Members of the group can't add any reactions to messages." + } } + entries.append(.allowAllInfo(allInfoText)) - entries.append(.itemsHeader(presentationData.strings.PeerInfo_AllowedReactions_ReactionListHeader)) - var index = 0 - for availableReaction in availableReactions.reactions { - if !availableReaction.isEnabled { - continue + if mode == .some { + entries.append(.itemsHeader(presentationData.strings.PeerInfo_AllowedReactions_ReactionListHeader)) + var index = 0 + for availableReaction in availableReactions.reactions { + if !availableReaction.isEnabled { + continue + } + entries.append(.item(index: index, value: availableReaction.value, availableReactions: availableReactions, reaction: availableReaction.value, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value))) + index += 1 } - entries.append(.item(index: index, value: availableReaction.value, availableReactions: availableReactions, reaction: availableReaction.value, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value))) - index += 1 } } @@ -196,18 +318,37 @@ public func peerAllowedReactionListController( let _ = dismissImpl let actionsDisposable = DisposableSet() - actionsDisposable.add((context.engine.data.get(TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: peerId)) - |> deliverOnMainQueue).start(next: { allowedReactions in + actionsDisposable.add((combineLatest(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: peerId)), context.engine.stickers.availableReactions() |> take(1)) + |> deliverOnMainQueue).start(next: { allowedReactions, availableReactions in updateState { state in var state = state - state.updatedAllowedReactions = allowedReactions.flatMap(Set.init) + + if allowedReactions == nil { + state.updatedMode = .all + if let availableReactions = availableReactions { + let updatedAllowedReactions = availableReactions.reactions.map { $0.value } + state.updatedAllowedReactions = Set(updatedAllowedReactions) + } + } else if let allowedReactions = allowedReactions, !allowedReactions.isEmpty { + if let availableReactions = availableReactions, Set(allowedReactions) == Set(availableReactions.reactions.map(\.value)) { + state.updatedMode = .all + } else { + state.updatedMode = .some + } + let updatedAllowedReactions = Set(allowedReactions) + state.updatedAllowedReactions = updatedAllowedReactions + } else { + state.updatedMode = .empty + state.updatedAllowedReactions = Set() + } + return state } })) let arguments = PeerAllowedReactionListControllerArguments( context: context, - toggleAll: { + setMode: { mode in let _ = (context.engine.stickers.availableReactions() |> take(1) |> deliverOnMainQueue).start(next: { availableReactions in @@ -216,19 +357,26 @@ public func peerAllowedReactionListController( } updateState { state in var state = state + state.updatedMode = mode + if var updatedAllowedReactions = state.updatedAllowedReactions { - if updatedAllowedReactions.isEmpty { + switch mode { + case .all: + updatedAllowedReactions.removeAll() for availableReaction in availableReactions.reactions { if !availableReaction.isEnabled { continue } updatedAllowedReactions.insert(availableReaction.value) } - } else { + case .some: + updatedAllowedReactions.removeAll() + case .empty: updatedAllowedReactions.removeAll() } state.updatedAllowedReactions = updatedAllowedReactions } + return state } }) @@ -283,7 +431,7 @@ public func peerAllowedReactionListController( presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, - animateChanges: true + animateChanges: false ) return (controllerState, (listState, arguments)) diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift index a9c5c4595f..2a26508fb9 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift @@ -490,7 +490,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController { chatController.canReadHistory.set(false) let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: peerNearbyContextMenuItems(context: context, peerId: peer.id, present: { c in presentControllerImpl?(c, nil) - }) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + }) |> map { ContextController.Items(content: .list($0), animationCache: nil) }, gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, expandUsers: { expandedPromise.set(true) diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index cbb0e38f16..d2eddb60c5 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -93,6 +93,8 @@ swift_library( "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/LegacyComponents:LegacyComponents", "//submodules/CheckNode:CheckNode", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift index 740e05f6f8..c4e960f2f3 100644 --- a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift @@ -9,6 +9,9 @@ import AccountContext import ReactionSelectionNode import TelegramPresentationData import AccountContext +import AnimationCache +import Postbox +import MultiAnimationRenderer final class ReactionsCarouselComponent: Component { public typealias EnvironmentType = DemoPageEnvironment @@ -117,10 +120,18 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { private var previousInteractionTimestamp: Double = 0.0 private var timer: SwiftSignalKit.Timer? + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + init(context: AccountContext, theme: PresentationTheme, reactions: [AvailableReactions.Reaction]) { self.context = context self.theme = theme + self.animationCache = AnimationCacheImpl(basePath: self.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationRenderer = MultiAnimationRendererImpl() + var reactionMap: [MessageReaction.Reaction: AvailableReactions.Reaction] = [:] for reaction in reactions { reactionMap[reaction.value] = reaction @@ -341,6 +352,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { continue } let containerNode = ASDisplayNode() + let itemNode = ReactionNode(context: self.context, theme: self.theme, item: ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), appearAnimation: reaction.appearAnimation, @@ -350,7 +362,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { applicationAnimation: aroundAnimation, largeApplicationAnimation: reaction.effectAnimation, isCustom: false - ), hasAppearAnimation: false, useDirectRendering: false) + ), animationCache: self.animationCache, animationRenderer: self.animationRenderer, hasAppearAnimation: false, useDirectRendering: false) containerNode.isUserInteractionEnabled = false containerNode.addSubnode(itemNode) self.addSubnode(containerNode) @@ -402,7 +414,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { targetContainerNode.addSubnode(standaloneReactionAnimation) standaloneReactionAnimation.frame = targetContainerNode.bounds standaloneReactionAnimation.animateReactionSelection( - context: self.context, theme: self.theme, reaction: ReactionItem( + context: self.context, theme: self.theme, animationCache: self.animationCache, reaction: ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), appearAnimation: reaction.appearAnimation, stillAnimation: reaction.selectAnimation, diff --git a/submodules/ReactionSelectionNode/BUILD b/submodules/ReactionSelectionNode/BUILD index 3cff572c95..64d2108cf9 100644 --- a/submodules/ReactionSelectionNode/BUILD +++ b/submodules/ReactionSelectionNode/BUILD @@ -27,7 +27,11 @@ swift_library( "//submodules/ComponentFlow:ComponentFlow", "//submodules/TelegramUI/Components/EmojiStatusSelectionComponent:EmojiStatusSelectionComponent", "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index ece25cf123..2ed1bca108 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -3,6 +3,7 @@ import AsyncDisplayKit import Display import AnimatedStickerNode import TelegramCore +import Postbox import TelegramPresentationData import AccountContext import TelegramAnimatedStickerNode @@ -15,6 +16,8 @@ import ComponentFlow import EmojiStatusSelectionComponent import EntityKeyboard import ComponentDisplayAdapters +import AnimationCache +import MultiAnimationRenderer public final class ReactionItem { public struct Reaction: Equatable { @@ -80,11 +83,47 @@ public enum ReactionContextItem { private let largeCircleSize: CGFloat = 16.0 private let smallCircleSize: CGFloat = 8.0 +private final class ExpandItemView: UIView { + private let arrowView: UIImageView + let tintView: UIView + + override init(frame: CGRect) { + self.tintView = UIView() + self.tintView.backgroundColor = .white + + self.arrowView = UIImageView() + self.arrowView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReactionExpandArrow"), color: .white) + + super.init(frame: frame) + + self.addSubview(self.arrowView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateTheme(theme: PresentationTheme) { + self.backgroundColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.mixedWith(theme.contextMenu.backgroundColor.withMultipliedAlpha(0.4), alpha: 0.5) + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + self.layer.cornerRadius = size.width / 2.0 + self.tintView.layer.cornerRadius = size.width / 2.0 + + if let image = self.arrowView.image { + transition.updateFrame(view: self.arrowView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels((size.height - image.size.height) / 2.0)), size: image.size)) + } + } +} + public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private let context: AccountContext private let presentationData: PresentationData + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer private let items: [ReactionContextItem] - private let getEmojiContent: (() -> Signal)? + private let getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? private let isExpandedUpdated: (ContainedViewLayoutTransition) -> Void private let backgroundNode: ReactionContextBackgroundNode @@ -99,9 +138,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private let previewingItemContainer: ASDisplayNode private var visibleItemNodes: [Int: ReactionItemNode] = [:] private var visibleItemMaskNodes: [Int: ASDisplayNode] = [:] - private let expandItemView: UIView - private let expandItemVibrancyView: UIView - private let expandItemArrowView: UIImageView + private let expandItemView: ExpandItemView? private var reactionSelectionComponentHost: ComponentView? @@ -135,13 +172,16 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var emojiContent: EmojiPagerContentComponent? private var emojiContentDisposable: Disposable? - public init(context: AccountContext, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: (() -> Signal)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void) { + public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void) { self.context = context self.presentationData = presentationData self.items = items self.getEmojiContent = getEmojiContent self.isExpandedUpdated = isExpandedUpdated + self.animationCache = animationCache + self.animationRenderer = MultiAnimationRendererImpl() + self.backgroundMaskNode = ASDisplayNode() self.backgroundNode = ReactionContextBackgroundNode(largeCircleSize: largeCircleSize, smallCircleSize: smallCircleSize, maskNode: self.backgroundMaskNode) self.leftBackgroundMaskNode = ASDisplayNode() @@ -202,15 +242,19 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { context.setFillColor(shadowColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: gradientWidth - 1.0, dy: gradientWidth - 1.0)) })?.stretchableImage(withLeftCapWidth: Int(46.0 / 2.0), topCapHeight: Int(46.0 / 2.0)) - //self.contentContainer.view.mask = self.contentContainerMask + if self.getEmojiContent == nil { + self.contentContainer.view.mask = self.contentContainerMask + } - self.expandItemView = UIView() - self.expandItemVibrancyView = UIView() - self.expandItemArrowView = UIImageView() - self.expandItemArrowView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReactionExpandArrow"), color: .white) - self.expandItemView.addSubview(self.expandItemArrowView) - self.contentContainer.view.addSubview(self.expandItemView) - self.contentTintContainer.view.addSubview(self.expandItemVibrancyView) + if getEmojiContent != nil { + let expandItemView = ExpandItemView() + self.expandItemView = expandItemView + + self.contentContainer.view.addSubview(expandItemView) + self.contentTintContainer.view.addSubview(expandItemView.tintView) + } else { + self.expandItemView = nil + } super.init() @@ -330,9 +374,16 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { var currentMaskFrame: CGRect? var maskTransition: ContainedViewLayoutTransition? + let topVisibleItems: Int + if self.getEmojiContent != nil { + topVisibleItems = min(self.items.count, visibleItemCount - 1) + } else { + topVisibleItems = self.items.count + } + var validIndices = Set() var nextX: CGFloat = sideInset - for i in 0 ..< min(self.items.count, visibleItemCount - 1) { + for i in 0 ..< topVisibleItems { var currentItemSize = itemSize if let highlightedReactionIndex = highlightedReactionIndex { if highlightedReactionIndex == i { @@ -374,7 +425,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemTransition = .immediate if case let .reaction(item) = self.items[i] { - itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item) + itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, animationCache: self.animationCache, animationRenderer: self.animationRenderer) maskNode = nil } else { itemNode = PremiumReactionsNode(theme: self.presentationData.theme) @@ -426,16 +477,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - let baseNextFrame = CGRect(origin: CGPoint(x: nextX + 3.0, y: containerHeight - contentHeight + floor((contentHeight - 30.0) / 2.0) + (self.isExpanded ? 46.0 : 0.0)), size: CGSize(width: 30.0, height: 30.0)) - - transition.updateFrame(view: self.expandItemView, frame: baseNextFrame) - self.expandItemView.layer.cornerRadius = baseNextFrame.width / 2.0 - - transition.updateFrame(view: self.expandItemVibrancyView, frame: baseNextFrame) - self.expandItemVibrancyView.layer.cornerRadius = baseNextFrame.width / 2.0 - - if let image = self.expandItemArrowView.image { - self.expandItemArrowView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((baseNextFrame.width - image.size.width) / 2.0), y: floorToScreenPixels((baseNextFrame.height - image.size.height) / 2.0)), size: image.size) + if let expandItemView = self.expandItemView { + let baseNextFrame = CGRect(origin: CGPoint(x: nextX + 3.0, y: containerHeight - contentHeight + floor((contentHeight - 30.0) / 2.0) + (self.isExpanded ? 46.0 : 0.0)), size: CGSize(width: 30.0, height: 30.0)) + + transition.updateFrame(view: expandItemView, frame: baseNextFrame) + transition.updateFrame(view: expandItemView.tintView, frame: baseNextFrame) + + expandItemView.update(size: baseNextFrame.size, transition: transition) } if let currentMaskFrame = currentMaskFrame { @@ -466,8 +514,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } private func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, isAnimatingOut: Bool, transition: ContainedViewLayoutTransition, animateInFromAnchorRect: CGRect?, animateOutToAnchorRect: CGRect?, animateReactionHighlight: Bool = false) { - self.expandItemView.backgroundColor = self.presentationData.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.mixedWith(self.presentationData.theme.contextMenu.backgroundColor.withMultipliedAlpha(0.4), alpha: 0.5) - self.expandItemVibrancyView.backgroundColor = .white + if let expandItemView = self.expandItemView { + expandItemView.updateTheme(theme: self.presentationData.theme) + } self.validLayout = (size, insets, anchorRect) @@ -478,20 +527,36 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let verticalInset: CGFloat = 13.0 let rowHeight: CGFloat = 30.0 - let totalItemSlotCount = self.items.count + 1 + var itemCount: Int + var visibleContentWidth: CGFloat + var completeContentWidth: CGFloat - var maxRowItemCount = Int(floor((size.width - sideInset * 2.0 - externalSideInset * 2.0 - itemSpacing) / (itemSize + itemSpacing))) - maxRowItemCount = min(maxRowItemCount, 8) - var itemCount = min(totalItemSlotCount, maxRowItemCount) - if self.isExpanded { - itemCount = maxRowItemCount - } - - let minVisibleItemCount: CGFloat = CGFloat(itemCount) - let completeContentWidth = CGFloat(itemCount) * itemSize + (CGFloat(itemCount) - 1.0) * itemSpacing + sideInset * 2.0 - var visibleContentWidth = floor(minVisibleItemCount * itemSize + (minVisibleItemCount - 1.0) * itemSpacing + sideInset * 2.0) - if visibleContentWidth > size.width - sideInset * 2.0 { - visibleContentWidth = size.width - sideInset * 2.0 + if self.getEmojiContent != nil { + let totalItemSlotCount = self.items.count + 1 + + var maxRowItemCount = Int(floor((size.width - sideInset * 2.0 - externalSideInset * 2.0 - itemSpacing) / (itemSize + itemSpacing))) + maxRowItemCount = min(maxRowItemCount, 8) + itemCount = min(totalItemSlotCount, maxRowItemCount) + if self.isExpanded { + itemCount = maxRowItemCount + } + + let minVisibleItemCount: CGFloat = CGFloat(itemCount) + completeContentWidth = CGFloat(itemCount) * itemSize + (CGFloat(itemCount) - 1.0) * itemSpacing + sideInset * 2.0 + visibleContentWidth = floor(minVisibleItemCount * itemSize + (minVisibleItemCount - 1.0) * itemSpacing + sideInset * 2.0) + if visibleContentWidth > size.width - sideInset * 2.0 { + visibleContentWidth = size.width - sideInset * 2.0 + } + } else { + itemCount = self.items.count + completeContentWidth = floor(CGFloat(itemCount) * itemSize + (CGFloat(itemCount) - 1.0) * itemSpacing + sideInset * 2.0) + + let minVisibleItemCount = min(CGFloat(self.items.count), 6.5) + + visibleContentWidth = floor(minVisibleItemCount * itemSize + (minVisibleItemCount - 1.0) * itemSpacing + sideInset * 2.0) + if visibleContentWidth > size.width - sideInset * 2.0 { + visibleContentWidth = size.width - sideInset * 2.0 + } } let contentHeight = verticalInset * 2.0 + rowHeight @@ -529,7 +594,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { emojiContent = current } else { let semaphore = DispatchSemaphore(value: 0) - let _ = (getEmojiContent() |> take(1)).start(next: { value in + let _ = (getEmojiContent(self.animationCache, self.animationRenderer) |> take(1)).start(next: { value in emojiContent = value semaphore.signal() }) @@ -537,7 +602,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { semaphore.wait() self.emojiContent = emojiContent - self.emojiContentDisposable = (getEmojiContent() + self.emojiContentDisposable = (getEmojiContent(self.animationCache, self.animationRenderer) |> deliverOnMainQueue).start(next: { [weak self] emojiContent in guard let strongSelf = self else { return @@ -643,23 +708,44 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemNode.isHidden = true } if let emojiView = reactionSelectionComponentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View { - emojiView.animateInReactionSelection(stationaryItemCount: itemCount - 1) + var initialPositionAndFrame: [MediaId: (position: CGPoint, frameIndex: Int, placeholder: UIImage)] = [:] + for (_, itemNode) in self.visibleItemNodes { + guard let itemNode = itemNode as? ReactionNode else { + continue + } + guard let placeholder = itemNode.currentFrameImage else { + continue + } + initialPositionAndFrame[itemNode.item.stillAnimation.fileId] = ( + position: itemNode.frame.center, + frameIndex: itemNode.currentFrameIndex, + placeholder: placeholder + ) + } + + emojiView.animateInReactionSelection(sourceItems: initialPositionAndFrame) if let mirrorContentClippingView = emojiView.mirrorContentClippingView { Transition(transition).animateBoundsOrigin(view: mirrorContentClippingView, from: CGPoint(x: 0.0, y: 46.0), to: CGPoint(), additive: true) } } - self.expandItemView.alpha = 0.0 - self.expandItemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.scrollNode.isHidden = true - }) - self.expandItemView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.expandItemVibrancyView.alpha = 0.0 - self.expandItemVibrancyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) - self.expandItemVibrancyView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + if let topPanelView = reactionSelectionComponentHost.findTaggedView(tag: EntityKeyboardTopPanelComponent.Tag(id: AnyHashable("emoji"))) as? EntityKeyboardTopPanelComponent.View { + topPanelView.animateIn() + } + + if let expandItemView = self.expandItemView { + expandItemView.alpha = 0.0 + expandItemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.scrollNode.isHidden = true + }) + expandItemView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + expandItemView.tintView.alpha = 0.0 + expandItemView.tintView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + expandItemView.tintView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } animateIn = true } @@ -739,8 +825,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } let itemDelay = mainCircleDelay + Double(self.visibleItemNodes.count) * 0.06 - self.expandItemView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: itemDelay) - self.expandItemVibrancyView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: itemDelay) + if let expandItemView = self.expandItemView { + expandItemView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: itemDelay) + expandItemView.tintView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: itemDelay) + } } else { for i in 0 ..< self.items.count { guard let itemNode = self.visibleItemNodes[i] else { @@ -765,8 +853,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { reactionComponentView.alpha = 0.0 reactionComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } - self.expandItemView.alpha = 0.0 - self.expandItemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + if let expandItemView = self.expandItemView { + expandItemView.alpha = 0.0 + expandItemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + expandItemView.tintView.alpha = 0.0 + expandItemView.tintView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } if let targetAnchorRect = targetAnchorRect, let (size, insets, anchorRect) = self.validLayout { self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: targetAnchorRect) @@ -780,7 +872,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } let sourceFrame = itemNode.view.convert(itemNode.bounds, to: self.view) - let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil) + + var selfTargetBounds = targetView.bounds + if case .builtin = itemNode.item.reaction.rawValue { + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + } + + let targetFrame = self.view.convert(targetView.convert(selfTargetBounds, to: nil), from: nil) targetSnapshotView.frame = targetFrame self.view.insertSubview(targetSnapshotView, belowSubview: itemNode.view) @@ -811,7 +909,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { targetView.alpha = 1.0 targetView.isHidden = false if let targetView = targetView as? ReactionIconView { - targetView.imageView.alpha = 1.0 + targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) } targetSnapshotView?.isHidden = true targetScaleCompleted = true @@ -842,15 +940,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - let isStillImage: Bool if let customReactionSource = self.customReactionSource { - if customReactionSource.item.listAnimation.isVideoEmoji || customReactionSource.item.listAnimation.isVideoSticker || customReactionSource.item.listAnimation.isAnimatedSticker { - isStillImage = false - } else { - isStillImage = true - } - - let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, useDirectRendering: false) + let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, useDirectRendering: false) if let contents = customReactionSource.layer.contents { itemNode.setCustomContents(contents: contents) } @@ -859,8 +950,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemNode.updateLayout(size: itemNode.frame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: .immediate) customReactionSource.layer.isHidden = true foundItemNode = itemNode - } else { - isStillImage = false } guard let itemNode = foundItemNode else { @@ -868,6 +957,18 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { return } + let switchToInlineImmediately: Bool + if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker { + switch itemNode.item.reaction.rawValue { + case .builtin: + switchToInlineImmediately = false + case .custom: + switchToInlineImmediately = true + } + } else { + switchToInlineImmediately = true + } + self.animationTargetView = targetView self.animationHideNode = hideNode @@ -887,7 +988,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemNode.isExtracted = true let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view) - let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) + + var selfTargetBounds = targetView.bounds + if case .builtin = itemNode.item.reaction.rawValue { + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + } + + let selfTargetRect = self.view.convert(selfTargetBounds, from: targetView) var expandedSize: CGSize = selfTargetRect.size if self.didTriggerExpandedReaction { @@ -914,7 +1021,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let additionalAnimationNode: DefaultAnimatedStickerNodeImpl? let additionalAnimation: TelegramMediaFile? - if self.didTriggerExpandedReaction, !isStillImage { + if self.didTriggerExpandedReaction, !switchToInlineImmediately { additionalAnimation = itemNode.item.largeApplicationAnimation } else { additionalAnimation = itemNode.item.applicationAnimation @@ -974,12 +1081,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { targetView.isHidden = false targetView.alpha = 1.0 - if isStillImage { - targetView.imageView.alpha = 1.0 + if switchToInlineImmediately { + targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) } else { - targetView.imageView.alpha = 0.0 + targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) targetView.addSubnode(itemNode) - itemNode.frame = targetView.bounds + itemNode.frame = selfTargetBounds } if strongSelf.hapticFeedback == nil { @@ -987,13 +1094,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } strongSelf.hapticFeedback?.tap() - if isStillImage { + if switchToInlineImmediately { mainAnimationCompleted = true intermediateCompletion() } } - if isStillImage { + if switchToInlineImmediately { afterCompletion() } else { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: afterCompletion) @@ -1009,7 +1116,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } }) - if !isStillImage { + if !switchToInlineImmediately { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, 2.0 * UIView.animationDurationFactor()), execute: { if self.didTriggerExpandedReaction { self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { [weak self] in @@ -1021,6 +1128,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { standaloneReactionAnimation.animateReactionSelection( context: strongSelf.context, theme: strongSelf.context.sharedContext.currentPresentationData.with({ $0 }).theme, + animationCache: strongSelf.animationCache, reaction: itemNode.item, avatarPeers: [], playHaptic: false, @@ -1041,7 +1149,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { targetView.alpha = 1.0 targetView.isHidden = false if let targetView = targetView as? ReactionIconView { - targetView.imageView.alpha = 1.0 + targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) itemNode.removeFromSupernode() } } @@ -1125,7 +1233,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { case .ended: let point = recognizer.location(in: self.view) - if self.expandItemView.bounds.contains(self.view.convert(point, to: self.expandItemView)) { + if let expandItemView = self.expandItemView, expandItemView.bounds.contains(self.view.convert(point, to: self.expandItemView)) { self.currentContentHeight = 300.0 self.isExpanded = true self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring)) @@ -1279,13 +1387,13 @@ public final class StandaloneReactionAnimation: ASDisplayNode { self.isUserInteractionEnabled = false } - public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { - self.animateReactionSelection(context: context, theme: theme, reaction: reaction, avatarPeers: avatarPeers, playHaptic: playHaptic, isLarge: isLarge, forceSmallEffectAnimation: forceSmallEffectAnimation, targetView: targetView, addStandaloneReactionAnimation: addStandaloneReactionAnimation, currentItemNode: nil, completion: completion) + public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { + self.animateReactionSelection(context: context, theme: theme, animationCache: animationCache, reaction: reaction, avatarPeers: avatarPeers, playHaptic: playHaptic, isLarge: isLarge, forceSmallEffectAnimation: forceSmallEffectAnimation, targetView: targetView, addStandaloneReactionAnimation: addStandaloneReactionAnimation, currentItemNode: nil, completion: completion) } public var currentDismissAnimation: (() -> Void)? - public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, currentItemNode: ReactionNode?, completion: @escaping () -> Void) { + public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, currentItemNode: ReactionNode?, completion: @escaping () -> Void) { guard let sourceSnapshotView = targetView.snapshotContentTree() else { completion() return @@ -1301,10 +1409,11 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if let currentItemNode = currentItemNode { itemNode = currentItemNode } else { - itemNode = ReactionNode(context: context, theme: theme, item: reaction) + let animationRenderer = MultiAnimationRendererImpl() + itemNode = ReactionNode(context: context, theme: theme, item: reaction, animationCache: animationCache, animationRenderer: animationRenderer) } self.itemNode = itemNode - + if !forceSmallEffectAnimation { if let targetView = targetView as? ReactionIconView, !isLarge { self.itemNodeIsEmbedded = true @@ -1321,14 +1430,20 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if let targetView = targetView as? ReactionIconView, !isLarge { strongSelf.itemNodeIsEmbedded = true - targetView.imageView.isHidden = true + targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) } else { targetView.isHidden = true } } itemNode.isExtracted = true - let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) + + var selfTargetBounds = targetView.bounds + if case .builtin = itemNode.item.reaction.rawValue { + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + } + + let selfTargetRect = self.view.convert(selfTargetBounds, from: targetView) var expandedSize: CGSize = selfTargetRect.size if isLarge { @@ -1356,7 +1471,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } if self.itemNodeIsEmbedded { - itemNode.frame = targetView.bounds + itemNode.frame = selfTargetBounds } else { itemNode.frame = expandedFrame @@ -1485,6 +1600,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { standaloneReactionAnimation.animateReactionSelection( context: itemNode.context, theme: itemNode.context.sharedContext.currentPresentationData.with({ $0 }).theme, + animationCache: animationCache, reaction: itemNode.item, avatarPeers: avatarPeers, playHaptic: false, @@ -1503,7 +1619,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } else { if let targetView = strongSelf.targetView { if let targetView = targetView as? ReactionIconView, !isLarge { - targetView.imageView.isHidden = false + targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) } else { targetView.alpha = 1.0 targetView.isHidden = false @@ -1529,10 +1645,10 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } if forceSmallEffectAnimation { - itemNode.mainAnimationCompletion = { + //itemNode.mainAnimationCompletion = { mainAnimationCompleted = true maybeBeginDismissAnimation() - } + //} } if let additionalAnimationNode = additionalAnimationNode { @@ -1567,7 +1683,17 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } let sourceFrame = itemNode.view.convert(itemNode.bounds, to: self.view) - let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil) + + var selfTargetBounds = targetView.bounds + if let itemNode = self.itemNode, case .builtin = itemNode.item.reaction.rawValue { + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + } + + var targetFrame = self.view.convert(targetView.convert(selfTargetBounds, to: nil), from: nil) + + if let itemNode = self.itemNode, case .builtin = itemNode.item.reaction.rawValue { + targetFrame = targetFrame.insetBy(dx: -targetFrame.width * 0.5, dy: -targetFrame.height * 0.5) + } targetSnapshotView.frame = targetFrame self.view.insertSubview(targetSnapshotView, belowSubview: itemNode.view) @@ -1598,7 +1724,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { targetView.alpha = 1.0 targetView.isHidden = false if let targetView = targetView as? ReactionIconView { - targetView.imageView.alpha = 1.0 + targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) } targetSnapshotView?.isHidden = true targetScaleCompleted = true @@ -1622,7 +1748,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if let targetView = self.targetView { if let targetView = targetView as? ReactionIconView, self.itemNodeIsEmbedded { - targetView.imageView.isHidden = false + targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) } else { targetView.alpha = 1.0 targetView.isHidden = false diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index c03979ca5c..11addcef5a 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -10,6 +10,8 @@ import TelegramAnimatedStickerNode import SwiftSignalKit import StickerResources import AccountContext +import AnimationCache +import MultiAnimationRenderer 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 @@ -69,7 +71,15 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { var expandedAnimationDidBegin: (() -> Void)? - public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) { + var currentFrameIndex: Int { + return self.staticAnimationNode.currentFrameIndex + } + + var currentFrameImage: UIImage? { + return self.staticAnimationNode.currentFrameImage + } + + public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) { self.context = context self.item = item self.hasAppearAnimation = hasAppearAnimation @@ -95,6 +105,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { } if strongSelf.animationNode == nil { strongSelf.staticAnimationNode.isHidden = false + strongSelf.staticAnimationNode.playLoop() } strongSelf.animateInAnimationNode?.removeFromSupernode() @@ -126,8 +137,6 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { } } - public var mainAnimationCompletion: (() -> Void)? - public func setCustomContents(contents: Any) { if self.customContentsNode == nil { let customContentsNode = ASDisplayNode() @@ -155,9 +164,6 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { let expandedAnimationFrame = animationFrame if isExpanded && !self.hasAppearAnimation { - self.staticAnimationNode.completed = { [weak self] _ in - self?.mainAnimationCompletion?() - } self.staticAnimationNode.play(firstFrame: false, fromIndex: 0) } else if isExpanded, self.animationNode == nil { let animationNode: AnimatedStickerNode = self.useDirectRendering ? DirectAnimatedStickerNode() : DefaultAnimatedStickerNodeImpl() @@ -172,9 +178,6 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { self?.expandedAnimationDidBegin?() } } - animationNode.completed = { [weak self] _ in - self?.mainAnimationCompletion?() - } if largeExpanded { let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource, isVideo: self.item.largeListAnimation.isVideoSticker || self.item.largeListAnimation.isVideoEmoji) @@ -281,6 +284,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { animateInAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1) strongSelf.staticAnimationNode.isHidden = false + strongSelf.staticAnimationNode.playLoop() } } stillAnimationNode.visibility = true @@ -319,6 +323,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { if self.animationNode == nil { self.didSetupStillAnimation = true + self.staticAnimationNode.automaticallyLoadFirstFrame = true if !self.hasAppearAnimation { self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource), 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))) } else { diff --git a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift index ffc83cb669..f3b82c5f6d 100644 --- a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift @@ -12,6 +12,7 @@ import PresentationDataUtils import AccountContext import WallpaperBackgroundNode import ReactionSelectionNode +import AnimationCache class ReactionChatPreviewItem: ListViewItem, ItemListItem { let context: AccountContext @@ -89,6 +90,8 @@ class ReactionChatPreviewItemNode: ListViewItemNode { private var item: ReactionChatPreviewItem? private(set) weak var standaloneReactionAnimation: StandaloneReactionAnimation? + private var animationCache: AnimationCache? + init() { self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -166,10 +169,20 @@ class ReactionChatPreviewItemNode: ListViewItemNode { let standaloneReactionAnimation = StandaloneReactionAnimation() self.standaloneReactionAnimation = standaloneReactionAnimation + let animationCache: AnimationCache + if let current = self.animationCache { + animationCache = current + } else { + animationCache = AnimationCacheImpl(basePath: item.context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + self.animationCache = animationCache + } + supernode.addSubnode(standaloneReactionAnimation) standaloneReactionAnimation.frame = supernode.bounds standaloneReactionAnimation.animateReactionSelection( - context: item.context, theme: item.theme, reaction: ReactionItem( + context: item.context, theme: item.theme, animationCache: animationCache, reaction: ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), appearAnimation: reaction.appearAnimation, stillAnimation: reaction.selectAnimation, diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index e014df4d9b..aa0e4a5c34 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -233,7 +233,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } })) ]) - return ContextController.Items(content: .list(items)) + return ContextController.Items(content: .list(items), animationCache: nil) } let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(ShareContextReferenceContentSource(sourceNode: node, customPosition: CGPoint(x: 0.0, y: -116.0))), items: items, gesture: gesture) contextController.immediateItemsTransitionAnimation = true diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift index 2d5d9b5607..e1471a539b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift @@ -80,16 +80,19 @@ private func mergeReactions(reactions: [MessageReaction], recentPeers: [Reaction var result = reactions var recentPeers = recentPeers + var pendingIndex: Int = Int(Int32.max - 100) for pendingReaction in pending { if let index = result.firstIndex(where: { $0.value == pendingReaction.value }) { var merged = result[index] if merged.chosenOrder == nil { - merged.chosenOrder = Int(Int32.max) + merged.chosenOrder = pendingIndex + pendingIndex += 1 merged.count += 1 } result[index] = merged } else { - result.append(MessageReaction(value: pendingReaction.value, count: 1, chosenOrder: Int(Int32.max))) + result.append(MessageReaction(value: pendingReaction.value, count: 1, chosenOrder: pendingIndex)) + pendingIndex += 1 } if let index = recentPeers.firstIndex(where: { $0.value == pendingReaction.value && $0.peerId == accountPeerId }) { diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index 5376742337..6b967aa477 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -93,6 +93,6 @@ extension UserLimitsConfiguration { self.maxUploadFileParts = getValue("upload_max_fileparts", orElse: defaultValue.maxUploadFileParts) self.maxAboutLength = getValue("about_length_limit", orElse: defaultValue.maxAboutLength) self.maxAnimatedEmojisInText = getGeneralValue("message_animated_emoji_max", orElse: defaultValue.maxAnimatedEmojisInText) - self.maxReactionsPerMessage = getGeneralValue("reactions_user_max", orElse: 1) + self.maxReactionsPerMessage = getValue("reactions_user_max", orElse: 1) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index bb0e45fe7e..afab22d498 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -67,7 +67,13 @@ public extension TelegramEngine { return self.account.postbox.transaction { transaction -> Void in if let file = file { transaction.storeMediaIfNotPresent(media: file) + + if let entry = CodableEntry(RecentMediaItem(file)) { + let itemEntry = OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry) + transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStatusEmoji, item: itemEntry, removeTailIfCountExceeds: 50) + } } + if let peer = transaction.getPeer(peerId) as? TelegramUser { updatePeers(transaction: transaction, peers: [peer.withUpdatedEmojiStatus(file.flatMap({ PeerEmojiStatus(fileId: $0.fileId.id) }))], update: { _, updated in updated diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index d015b7c0c8..8f9686b872 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -322,6 +322,17 @@ public extension Message { } return nil } + var effectiveReactions: [MessageReaction]? { + if !self.hasReactions { + return nil + } + + if let result = mergedMessageReactions(attributes: self.attributes) { + return result.reactions + } else { + return nil + } + } var hasReactions: Bool { for attribute in self.attributes { if let attribute = attribute as? ReactionsMessageAttribute { diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index c20bc18d9c..d0b09d7f3f 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -56,15 +56,21 @@ public final class AnimationCacheItem { public let numFrames: Int private let advanceImpl: (Advance, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? + private let resetImpl: () -> Void - public init(numFrames: Int, advanceImpl: @escaping (Advance, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?) { + public init(numFrames: Int, advanceImpl: @escaping (Advance, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?, resetImpl: @escaping () -> Void) { self.numFrames = numFrames self.advanceImpl = advanceImpl + self.resetImpl = resetImpl } public func advance(advance: Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? { return self.advanceImpl(advance, requestedFormat) } + + public func reset() { + self.resetImpl() + } } public struct AnimationCacheItemDrawingSurface { @@ -775,6 +781,10 @@ private final class AnimationCacheItemAccessor { } } + func reset() { + self.currentFrame = nil + } + func advance(advance: AnimationCacheItem.Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? { switch advance { case let .frames(count): @@ -1204,6 +1214,8 @@ private func loadItem(path: String) throws -> AnimationCacheItem { return AnimationCacheItem(numFrames: frameMapping.count, advanceImpl: { advance, requestedFormat in return itemAccessor.advance(advance: advance, requestedFormat: requestedFormat) + }, resetImpl: { + itemAccessor.reset() }) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index fc7651eb29..541577f16f 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -2137,6 +2137,7 @@ public final class EmojiPagerContentComponent: Component { private let content: ItemContent private let placeholderColor: UIColor + let pixelSize: CGSize private let size: CGSize private var disposable: Disposable? private var fetchDisposable: Disposable? @@ -2176,6 +2177,7 @@ public final class EmojiPagerContentComponent: Component { let scale = min(2.0, UIScreenScale) let pixelSize = CGSize(width: pointSize.width * scale, height: pointSize.height * scale) + self.pixelSize = pixelSize self.size = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale) super.init() @@ -2298,6 +2300,7 @@ public final class EmojiPagerContentComponent: Component { self.content = layer.content self.placeholderColor = layer.placeholderColor self.size = layer.size + self.pixelSize = layer.pixelSize self.onUpdateDisplayPlaceholder = { _, _ in } @@ -2646,23 +2649,45 @@ public final class EmojiPagerContentComponent: Component { } } - public func animateInReactionSelection(stationaryItemCount: Int) { - guard let component = self.component else { + public func animateInReactionSelection(sourceItems: [MediaId: (position: CGPoint, frameIndex: Int, placeholder: UIImage)]) { + guard let component = self.component, let itemLayout = self.itemLayout else { return } - var stationaryItemIds = Set() - if let group = component.itemGroups.first { - for item in group.items { - stationaryItemIds.insert(ItemLayer.Key( - groupId: group.groupId, - itemId: item.content.id - )) + + for (_, itemLayer) in self.visibleItemLayers { + guard case let .animation(animationData) = itemLayer.item.content else { + continue + } + guard let file = itemLayer.item.itemFile else { + continue + } + if let sourceItem = sourceItems[file.fileId] { + component.animationRenderer.setFrameIndex(itemId: animationData.resource.resource.id.stringRepresentation, size: itemLayer.pixelSize, frameIndex: sourceItem.frameIndex, placeholder: sourceItem.placeholder) + } else { + let distance = itemLayer.position.y - itemLayout.frame(groupIndex: 0, itemIndex: 0).midY + let maxDistance = self.bounds.height + let clippedDistance = max(0.0, min(distance, maxDistance)) + let distanceNorm = clippedDistance / maxDistance + + let delay = listViewAnimationCurveSystem(distanceNorm) * 0.16 + + itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) + itemLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, delay: delay) } } - for (key, itemLayer) in self.visibleItemLayers { - if !stationaryItemIds.contains(key) { - itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } + + for (_, groupHeader) in self.visibleGroupHeaders { + let distance = groupHeader.layer.position.y - itemLayout.frame(groupIndex: 0, itemIndex: 0).midY + let maxDistance = self.bounds.height + let clippedDistance = max(0.0, min(distance, maxDistance)) + let distanceNorm = clippedDistance / maxDistance + + let delay = listViewAnimationCurveSystem(distanceNorm) * 0.16 + + groupHeader.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) + groupHeader.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: delay) + groupHeader.tintContentLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) + groupHeader.tintContentLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: delay) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 258e9c1827..74fadba6ac 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -308,6 +308,7 @@ public final class EntityKeyboardComponent: Component { defaultActiveGifItemId = AnyHashable(value) } contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent( + id: "gifs", theme: component.theme, items: topGifItems, containerSideInset: component.containerInsets.left + component.topPanelInsets.left, @@ -417,6 +418,7 @@ public final class EntityKeyboardComponent: Component { } contents.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(stickerContent))) contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( + id: "stickers", theme: component.theme, items: topStickerItems, containerSideInset: component.containerInsets.left + component.topPanelInsets.left, @@ -520,6 +522,7 @@ public final class EntityKeyboardComponent: Component { } } contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( + id: "emoji", theme: component.theme, items: topEmojiItems, containerSideInset: component.containerInsets.left + component.topPanelInsets.left, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift index 3f7b663927..8d76d38cbc 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -7,7 +7,7 @@ import TelegramPresentationData import TelegramCore import Postbox -final class EntityKeyboardTopContainerPanelEnvironment: Equatable { +public final class EntityKeyboardTopContainerPanelEnvironment: Equatable { let visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)> let isExpandedUpdated: (Bool, Transition) -> Void @@ -19,7 +19,7 @@ final class EntityKeyboardTopContainerPanelEnvironment: Equatable { self.isExpandedUpdated = isExpandedUpdated } - static func ==(lhs: EntityKeyboardTopContainerPanelEnvironment, rhs: EntityKeyboardTopContainerPanelEnvironment) -> Bool { + public static func ==(lhs: EntityKeyboardTopContainerPanelEnvironment, rhs: EntityKeyboardTopContainerPanelEnvironment) -> Bool { if lhs.visibilityFractionUpdated !== rhs.visibilityFractionUpdated { return false } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index d3d25312d9..74426bb7cd 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -1132,8 +1132,8 @@ private final class ReorderGestureRecognizer: UIGestureRecognizer { } } -final class EntityKeyboardTopPanelComponent: Component { - typealias EnvironmentType = EntityKeyboardTopContainerPanelEnvironment +public final class EntityKeyboardTopPanelComponent: Component { + public typealias EnvironmentType = EntityKeyboardTopContainerPanelEnvironment final class Item: Equatable { let id: AnyHashable @@ -1161,6 +1161,7 @@ final class EntityKeyboardTopPanelComponent: Component { } } + let id: AnyHashable let theme: PresentationTheme let items: [Item] let containerSideInset: CGFloat @@ -1170,6 +1171,7 @@ final class EntityKeyboardTopPanelComponent: Component { let reorderItems: ([Item]) -> Void init( + id: AnyHashable, theme: PresentationTheme, items: [Item], containerSideInset: CGFloat, @@ -1178,6 +1180,7 @@ final class EntityKeyboardTopPanelComponent: Component { activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)>, reorderItems: @escaping ([Item]) -> Void ) { + self.id = id self.theme = theme self.items = items self.containerSideInset = containerSideInset @@ -1187,7 +1190,10 @@ final class EntityKeyboardTopPanelComponent: Component { self.reorderItems = reorderItems } - static func ==(lhs: EntityKeyboardTopPanelComponent, rhs: EntityKeyboardTopPanelComponent) -> Bool { + public static func ==(lhs: EntityKeyboardTopPanelComponent, rhs: EntityKeyboardTopPanelComponent) -> Bool { + if lhs.id != rhs.id { + return false + } if lhs.theme !== rhs.theme { return false } @@ -1210,7 +1216,15 @@ final class EntityKeyboardTopPanelComponent: Component { return true } - final class View: UIView, UIScrollViewDelegate { + public final class Tag { + public let id: AnyHashable + + public init(id: AnyHashable) { + self.id = id + } + } + + public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView { private struct ItemLayout { struct ItemDescription { var isStatic: Bool @@ -1457,6 +1471,21 @@ final class EntityKeyboardTopPanelComponent: Component { fatalError("init(coder:) has not been implemented") } + public func matches(tag: Any) -> Bool { + if let tag = tag as? Tag { + if tag.id == self.component?.id { + return true + } + } + return false + } + + public func animateIn() { + for (_, itemView) in self.itemViews { + itemView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.3, delay: 0.12) + } + } + public func scrollViewDidScroll(_ scrollView: UIScrollView) { if self.ignoreScrolling { return @@ -2095,11 +2124,11 @@ final class EntityKeyboardTopPanelComponent: Component { } } - func makeView() -> View { + public func makeView() -> View { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift index 5bae04719f..29f2465503 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift @@ -835,6 +835,9 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { return EmptyDisposable } + public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { + } + private func animationTick() { let secondsPerFrame = Double(self.frameSkip) / 60.0 diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 9e81281dac..0927adc1b7 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -9,6 +9,7 @@ public protocol MultiAnimationRenderer: AnyObject { func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable + func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) } private var nextRenderTargetId: Int64 = 1 @@ -230,9 +231,11 @@ private final class ItemAnimationContext { private var disposable: Disposable? private var displayLink: ConstantDisplayLinkAnimator? private var item: Atomic? + private var itemPlaceholderAndFrameIndex: (UIImage, Int)? private var currentFrame: Frame? - private var isLoadingFrame: Bool = false + private var loadingFrameTaskId: Int? + private var nextLoadingFrameTaskId: Int = 0 private(set) var isPlaying: Bool = false { didSet { @@ -257,6 +260,10 @@ private final class ItemAnimationContext { if let item = result.item { strongSelf.item = Atomic(value: item) } + if let (placeholder, index) = strongSelf.itemPlaceholderAndFrameIndex { + strongSelf.itemPlaceholderAndFrameIndex = nil + strongSelf.setFrameIndex(index: index, placeholder: placeholder) + } strongSelf.updateIsPlaying() } }) @@ -267,6 +274,45 @@ private final class ItemAnimationContext { self.displayLink?.invalidate() } + func setFrameIndex(index: Int, placeholder: UIImage) { + if let item = self.item { + let nextFrame = item.with { item -> AnimationCacheItemFrame? in + item.reset() + for i in 0 ... index { + let result = item.advance(advance: .frames(1), requestedFormat: .rgba) + if i == index { + return result + } + } + return nil + } + + self.loadingFrameTaskId = nil + + if let nextFrame = nextFrame, let currentFrame = Frame(frame: nextFrame) { + self.currentFrame = currentFrame + + for target in self.targets.copyItems() { + if let target = target.value { + target.transitionToContents(currentFrame.image.cgImage!) + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } + } + } + } else { + for target in self.targets.copyItems() { + if let target = target.value { + target.transitionToContents(placeholder.cgImage!) + } + } + + self.itemPlaceholderAndFrameIndex = (placeholder, index) + } + } + func updateAddedTarget(target: MultiAnimationRenderTarget) { if let currentFrame = self.currentFrame { if let cgImage = currentFrame.image.cgImage { @@ -313,7 +359,7 @@ private final class ItemAnimationContext { } var frameAdvance: AnimationCacheItem.Advance? - if !self.isLoadingFrame { + if self.loadingFrameTaskId == nil { if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 { let divisionFactor = advanceTimestamp / currentFrame.remainingDuration let wholeFactor = round(divisionFactor) @@ -331,8 +377,11 @@ private final class ItemAnimationContext { } } - if let frameAdvance = frameAdvance, !self.isLoadingFrame { - self.isLoadingFrame = true + if let frameAdvance = frameAdvance, self.loadingFrameTaskId == nil { + let taskId = self.nextLoadingFrameTaskId + self.nextLoadingFrameTaskId += 1 + + self.loadingFrameTaskId = taskId return LoadFrameGroupTask(task: { [weak self] in let currentFrame: Frame? @@ -352,7 +401,11 @@ private final class ItemAnimationContext { return } - strongSelf.isLoadingFrame = false + if strongSelf.loadingFrameTaskId != taskId { + return + } + + strongSelf.loadingFrameTaskId = nil if let currentFrame = currentFrame { strongSelf.currentFrame = currentFrame @@ -529,6 +582,12 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { }) } + func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { + if let itemContext = self.itemContexts[ItemKey(id: itemId, width: Int(size.width), height: Int(size.height))] { + itemContext.setFrameIndex(index: frameIndex, placeholder: placeholder) + } + } + private func updateIsPlaying() { var isPlaying = false for (_, itemContext) in self.itemContexts { @@ -660,6 +719,12 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion) } + public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { + if let groupContext = self.groupContext { + groupContext.setFrameIndex(itemId: itemId, size: size, frameIndex: frameIndex, placeholder: placeholder) + } + } + private func updateIsPlaying() { var isPlaying = false if let groupContext = self.groupContext { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 5575233382..6d9cda265c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1041,6 +1041,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } actions.context = strongSelf.context + + actions.animationCache = strongSelf.controllerInteraction?.presentationContext.animationCache let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) @@ -1112,24 +1114,42 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return nil } } - actions.getEmojiContent = { - guard let strongSelf = self else { - preconditionFailure() + + var allReactionsAreAvailable = false + switch allowedReactions { + case let .set(set): + if set == Set(availableReactions.reactions.filter(\.isEnabled).map(\.value)) { + allReactionsAreAvailable = true + } else { + allReactionsAreAvailable = false + } + case .all: + allReactionsAreAvailable = true + } + + if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info { + allReactionsAreAvailable = false + } + + if allReactionsAreAvailable { + actions.getEmojiContent = { animationCache, animationRenderer in + guard let strongSelf = self else { + preconditionFailure() + } + + return ChatEntityKeyboardInputNode.emojiInputData( + context: strongSelf.context, + animationCache: animationCache, + animationRenderer: animationRenderer, + isStandalone: false, + isStatusSelection: false, + isReactionSelection: true, + reactionItems: reactionItems, + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: strongSelf.chatLocation.peerId + ) } - - let presentationContext = strongSelf.controllerInteraction?.presentationContext - return ChatEntityKeyboardInputNode.emojiInputData( - context: strongSelf.context, - animationCache: presentationContext!.animationCache, - animationRenderer: presentationContext!.animationRenderer, - isStandalone: false, - isStatusSelection: false, - isReactionSelection: true, - reactionItems: reactionItems, - areUnicodeEmojiEnabled: false, - areCustomEmojiEnabled: true, - chatPeerId: strongSelf.chatLocation.peerId - ) } } } @@ -1171,66 +1191,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let action = { - guard let packReference = packReferences.first, let strongSelf = self else { + guard let strongSelf = self else { return } - strongSelf.chatDisplayNode.dismissTextInput() - - let presentationData = strongSelf.presentationData - let controller = StickerPackScreen(context: context, updatedPresentationData: strongSelf.updatedPresentationData, mainStickerPack: packReference, stickerPacks: Array(packReferences), parentNavigationController: strongSelf.effectiveNavigationController, actionPerformed: { [weak self] actions in - guard let strongSelf = self else { - return - } - if actions.count > 1, let first = actions.first { - if case .add = first.2 { - strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.EmojiPackActionInfo_AddedTitle, text: presentationData.strings.EmojiPackActionInfo_MultipleAddedText(Int32(actions.count)), undo: false, info: first.0, topItem: first.1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in - return true - })) - } else if actions.allSatisfy({ - if case .remove = $0.2 { - return true - } else { - return false - } - }) { - let isEmoji = actions[0].0.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks - - strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_MultipleRemovedText(Int32(actions.count)) : presentationData.strings.StickerPackActionInfo_MultipleRemovedText(Int32(actions.count)), undo: true, info: actions[0].0, topItem: actions[0].1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in - if case .undo = action { - var itemsAndIndices: [(StickerPackCollectionInfo, [StickerPackItem], Int)] = actions.compactMap { action -> (StickerPackCollectionInfo, [StickerPackItem], Int)? in - if case let .remove(index) = action.2 { - return (action.0, action.1, index) - } else { - return nil - } - } - itemsAndIndices.sort(by: { $0.2 < $1.2 }) - for (info, items, index) in itemsAndIndices.reversed() { - let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: index).start() - } - } - return true - })) - } - } else if let (info, items, action) = actions.first { - let isEmoji = info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks - - switch action { - case .add: - strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedTitle : presentationData.strings.StickerPackActionInfo_AddedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedText(info.title).string : presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in - return true - })) - case let .remove(positionInList): - strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedText(info.title).string : presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in - if case .undo = action { - let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).start() - } - return true - })) - } - } - }) - strongSelf.present(controller, in: .window(.root)) + strongSelf.presentEmojiList(references: packReferences) } if packReferences.count > 1 { @@ -1379,6 +1343,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = itemNode let _ = targetView }) + } else { + controller.dismiss() } }) } else { @@ -1444,15 +1410,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let _ = (strongSelf.context.engine.stickers.availableReactions() - |> deliverOnMainQueue).start(next: { availableReactions in + var customFileIds: [Int64] = [] + if case let .custom(fileId) = value { + customFileIds.append(fileId) + } + + let _ = (combineLatest( + strongSelf.context.engine.stickers.availableReactions(), + strongSelf.context.engine.stickers.resolveInlineStickers(fileIds: customFileIds) + ) + |> deliverOnMainQueue).start(next: { availableReactions, customEmoji in guard let strongSelf = self else { return } var dismissController: ((@escaping () -> Void) -> Void)? - let items = ContextController.Items(content: .custom(ReactionListContextMenuContent(context: strongSelf.context, availableReactions: availableReactions, message: EngineMessage(message), reaction: value, readStats: nil, back: nil, openPeer: { id in + var items = ContextController.Items(content: .custom(ReactionListContextMenuContent( + context: strongSelf.context, + availableReactions: availableReactions, + animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache, + animationRenderer: strongSelf.controllerInteraction!.presentationContext.animationRenderer, + message: EngineMessage(message), reaction: value, readStats: nil, back: nil, openPeer: { id in dismissController?({ guard let strongSelf = self else { return @@ -1462,8 +1441,125 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }))) + var packReferences: [StickerPackReference] = [] + var existingIds = Set() + for (_, file) in customEmoji { + loop: for attribute in file.attributes { + if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .id(id, _) = packReference, !existingIds.contains(id) { + packReferences.append(packReference) + existingIds.insert(id) + } + break loop + } + } + } + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let context = strongSelf.context + let presentationData = strongSelf.presentationData + + let action = { + guard let packReference = packReferences.first, let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.dismissTextInput() + + let presentationData = strongSelf.presentationData + let controller = StickerPackScreen(context: context, updatedPresentationData: strongSelf.updatedPresentationData, mainStickerPack: packReference, stickerPacks: Array(packReferences), parentNavigationController: strongSelf.effectiveNavigationController, actionPerformed: { [weak self] actions in + guard let strongSelf = self else { + return + } + if actions.count > 1, let first = actions.first { + if case .add = first.2 { + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.EmojiPackActionInfo_AddedTitle, text: presentationData.strings.EmojiPackActionInfo_MultipleAddedText(Int32(actions.count)), undo: false, info: first.0, topItem: first.1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in + return true + })) + } else if actions.allSatisfy({ + if case .remove = $0.2 { + return true + } else { + return false + } + }) { + let isEmoji = actions[0].0.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks + + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_MultipleRemovedText(Int32(actions.count)) : presentationData.strings.StickerPackActionInfo_MultipleRemovedText(Int32(actions.count)), undo: true, info: actions[0].0, topItem: actions[0].1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in + if case .undo = action { + var itemsAndIndices: [(StickerPackCollectionInfo, [StickerPackItem], Int)] = actions.compactMap { action -> (StickerPackCollectionInfo, [StickerPackItem], Int)? in + if case let .remove(index) = action.2 { + return (action.0, action.1, index) + } else { + return nil + } + } + itemsAndIndices.sort(by: { $0.2 < $1.2 }) + for (info, items, index) in itemsAndIndices.reversed() { + let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: index).start() + } + } + return true + })) + } + } else if let (info, items, action) = actions.first { + let isEmoji = info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks + + switch action { + case .add: + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedTitle : presentationData.strings.StickerPackActionInfo_AddedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedText(info.title).string : presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in + return true + })) + case let .remove(positionInList): + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedText(info.title).string : presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in + if case .undo = action { + let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).start() + } + return true + })) + } + } + }) + strongSelf.present(controller, in: .window(.root)) + } + + let presentationContext = strongSelf.controllerInteraction?.presentationContext + + if !packReferences.isEmpty { + items.tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil) + + if packReferences.count > 1 { + items.tip = .animatedEmoji(text: presentationData.strings.ChatContextMenu_EmojiSet(Int32(packReferences.count)), arguments: nil, file: nil, action: action) + } else if let reference = packReferences.first { + items.tipSignal = context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false) + |> filter { result in + if case .result = result { + return true + } else { + return false + } + } + |> mapToSignal { result -> Signal in + if case let .result(info, items, _) = result, let presentationContext = presentationContext { + let tip: ContextController.Tip = .animatedEmoji( + text: presentationData.strings.ChatContextMenu_EmojiSetSingle(info.title).string, + arguments: TextNodeWithEntities.Arguments( + context: context, + cache: presentationContext.animationCache, + renderer: presentationContext.animationRenderer, + placeholderColor: .clear, + attemptSynchronous: true + ), + file: items.first?.file, + action: action) + return .single(tip) |> delay(1.0, queue: .mainQueue()) + } else { + return .complete() + } + } + } + } + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: message, contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture) dismissController = { [weak controller] completion in @@ -1554,14 +1650,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - switch allowedReactions { - case let .set(set): - if !messageAlreadyHasThisReaction && updatedReactions.contains(where: { !set.contains($0) }) { - itemNode.openMessageContextMenu() - return + if removedReaction == nil { + switch allowedReactions { + case let .set(set): + if !messageAlreadyHasThisReaction && updatedReactions.contains(where: { !set.contains($0) }) { + itemNode.openMessageContextMenu() + return + } + case .all: + break } - case .all: - break } if removedReaction == nil && !updatedReactions.isEmpty { @@ -1593,6 +1691,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G standaloneReactionAnimation.animateReactionSelection( context: strongSelf.context, theme: strongSelf.presentationData.theme, + animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache, reaction: ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), appearAnimation: reaction.appearAnimation, @@ -1628,7 +1727,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts(itemNode: itemNode) - if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message) { + if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message, isPremium: strongSelf.presentationInterfaceState.isPremium) { var hideRemovedReaction: Bool = false if let reactions = mergedMessageReactions(attributes: message.attributes) { for reaction in reactions.reactions { @@ -6647,6 +6746,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G standaloneReactionAnimation.animateReactionSelection( context: strongSelf.context, theme: strongSelf.presentationData.theme, + animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache, reaction: ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), appearAnimation: reaction.appearAnimation, @@ -16653,6 +16753,70 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } + func presentEmojiList(references: [StickerPackReference]) { + guard let packReference = references.first else { + return + } + self.chatDisplayNode.dismissTextInput() + + let presentationData = self.presentationData + let controller = StickerPackScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, mainStickerPack: packReference, stickerPacks: Array(references), parentNavigationController: self.effectiveNavigationController, actionPerformed: { [weak self] actions in + guard let strongSelf = self else { + return + } + let context = strongSelf.context + if actions.count > 1, let first = actions.first { + if case .add = first.2 { + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.EmojiPackActionInfo_AddedTitle, text: presentationData.strings.EmojiPackActionInfo_MultipleAddedText(Int32(actions.count)), undo: false, info: first.0, topItem: first.1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in + return true + })) + } else if actions.allSatisfy({ + if case .remove = $0.2 { + return true + } else { + return false + } + }) { + let isEmoji = actions[0].0.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks + + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_MultipleRemovedText(Int32(actions.count)) : presentationData.strings.StickerPackActionInfo_MultipleRemovedText(Int32(actions.count)), undo: true, info: actions[0].0, topItem: actions[0].1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in + if case .undo = action { + var itemsAndIndices: [(StickerPackCollectionInfo, [StickerPackItem], Int)] = actions.compactMap { action -> (StickerPackCollectionInfo, [StickerPackItem], Int)? in + if case let .remove(index) = action.2 { + return (action.0, action.1, index) + } else { + return nil + } + } + itemsAndIndices.sort(by: { $0.2 < $1.2 }) + for (info, items, index) in itemsAndIndices.reversed() { + let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: index).start() + } + } + return true + })) + } + } else if let (info, items, action) = actions.first { + let isEmoji = info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks + + switch action { + case .add: + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedTitle : presentationData.strings.StickerPackActionInfo_AddedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedText(info.title).string : presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in + return true + })) + case let .remove(positionInList): + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedText(info.title).string : presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in + if case .undo = action { + let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).start() + } + return true + })) + } + } + }) + self.present(controller, in: .window(.root)) + } + public func hintPlayNextOutgoingGift() { self.controllerInteraction?.playNextOutgoingGift = true } diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index fbb18ec66c..f12a1e79b1 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -147,7 +147,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { recentEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedStatusEmoji { featuredStatusEmoji = orderedView - } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedStatusEmoji { + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStatusEmoji { recentStatusEmoji = orderedView } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 2b7c84e43b..08421b9bde 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1284,7 +1284,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: updateAllOnEachVersion) + var forceUpdateAll = false + if let previous = previous, previous.associatedData.isPremium != processedView.associatedData.isPremium { + forceUpdateAll = true + } + + let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: updateAllOnEachVersion || forceUpdateAll) var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, transition: rawTransition) if disableAnimations { @@ -1293,6 +1298,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { mappedTransition.options.remove(.AnimateTopItemPosition) mappedTransition.options.remove(.RequestItemInsertionAnimations) } + Queue.mainQueue().async { guard let strongSelf = self else { return @@ -2732,6 +2738,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { standaloneReactionAnimation.animateReactionSelection( context: self.context, theme: item.presentationData.theme.theme, + animationCache: self.controllerInteraction.presentationContext.animationCache, reaction: ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), appearAnimation: reaction.appearAnimation, diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index f6f8f30111..59d683228b 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -31,6 +31,7 @@ import ChatPresentationInterfaceState import Pasteboard import SettingsUI import PremiumUI +import TextNodeWithEntities private struct MessageContextMenuData { let starStatus: Bool? @@ -1580,19 +1581,54 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.insert(.separator, at: 0) } - actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, stats: readStats, action: { c, f, stats in + actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, stats: readStats, action: { c, f, stats, customReactionEmojiPacks, firstCustomEmojiReaction in if reactionCount == 0, let stats = stats, stats.peers.count == 1 { c.dismiss(completion: { controllerInteraction.openPeer(stats.peers[0].id, .default, nil, nil) }) } else if (stats != nil && !stats!.peers.isEmpty) || reactionCount != 0 { - c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent(context: context, availableReactions: availableReactions, message: EngineMessage(message), reaction: nil, readStats: stats, back: { [weak c] in - c?.popItems() - }, openPeer: { [weak c] id in - c?.dismiss(completion: { - controllerInteraction.openPeer(id, .default, nil, nil) + var tip: ContextController.Tip? + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + if customReactionEmojiPacks.count == 1, let firstCustomEmojiReaction = firstCustomEmojiReaction { + tip = .animatedEmoji( + text: presentationData.strings.ChatContextMenu_EmojiSetSingle(customReactionEmojiPacks[0].title).string, + arguments: TextNodeWithEntities.Arguments( + context: context, + cache: controllerInteraction.presentationContext.animationCache, + renderer: controllerInteraction.presentationContext.animationRenderer, + placeholderColor: .clear, + attemptSynchronous: true + ), + file: firstCustomEmojiReaction, + action: { + (interfaceInteraction.chatController() as? ChatControllerImpl)?.presentEmojiList(references: customReactionEmojiPacks.map { pack -> StickerPackReference in .id(id: pack.id.id, accessHash: pack.accessHash) }) + } + ) + } else if customReactionEmojiPacks.count > 1 { + tip = .animatedEmoji(text: presentationData.strings.ChatContextMenu_EmojiSet(Int32(customReactionEmojiPacks.count)), arguments: nil, file: nil, action: { + (interfaceInteraction.chatController() as? ChatControllerImpl)?.presentEmojiList(references: customReactionEmojiPacks.map { pack -> StickerPackReference in .id(id: pack.id.id, accessHash: pack.accessHash) }) }) - })), tip: nil))) + } + + c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent( + context: context, + availableReactions: availableReactions, + animationCache: controllerInteraction.presentationContext.animationCache, + animationRenderer: controllerInteraction.presentationContext.animationRenderer, + message: EngineMessage(message), + reaction: nil, + readStats: stats, + back: { [weak c] in + c?.popItems() + }, + openPeer: { [weak c] id in + c?.dismiss(completion: { + controllerInteraction.openPeer(id, .default, nil, nil) + }) + } + )), tip: tip))) } else { f(.default) } @@ -2125,9 +2161,9 @@ final class ChatReadReportContextItem: ContextMenuCustomItem { fileprivate let context: AccountContext fileprivate let message: Message fileprivate let stats: MessageReadStats? - fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?) -> Void + fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void - init(context: AccountContext, message: Message, stats: MessageReadStats?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?) -> Void) { + init(context: AccountContext, message: Message, stats: MessageReadStats?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void) { self.context = context self.message = message self.stats = stats @@ -2164,6 +2200,10 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus private var disposable: Disposable? private var currentStats: MessageReadStats? + + private var customEmojiPacksDisposable: Disposable? + private var customEmojiPacks: [StickerPackCollectionInfo] = [] + private var firstCustomEmojiReaction: TelegramMediaFile? init(presentationData: PresentationData, item: ChatReadReportContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { self.item = item @@ -2239,8 +2279,62 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) var reactionCount = 0 + var customEmojiFiles = Set() for reaction in mergedMessageReactionsAndPeers(message: self.item.message).reactions { reactionCount += Int(reaction.count) + + if case let .custom(fileId) = reaction.value { + customEmojiFiles.insert(fileId) + } + } + + if !customEmojiFiles.isEmpty { + self.customEmojiPacksDisposable = (item.context.engine.stickers.resolveInlineStickers(fileIds: Array(customEmojiFiles)) + |> mapToSignal { customEmoji -> Signal<([StickerPackCollectionInfo], TelegramMediaFile?), NoError> in + var stickerPackSignals: [Signal] = [] + var existingIds = Set() + var firstCustomEmojiReaction: TelegramMediaFile? + for (_, file) in customEmoji { + loop: for attribute in file.attributes { + if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .id(id, _) = packReference, !existingIds.contains(id) { + if firstCustomEmojiReaction == nil { + firstCustomEmojiReaction = file + } + + existingIds.insert(id) + stickerPackSignals.append(item.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) + |> filter { result in + if case .result = result { + return true + } else { + return false + } + } + |> map { result -> StickerPackCollectionInfo? in + if case let .result(info, _, _) = result { + return info + } else { + return nil + } + }) + } + break loop + } + } + } + return combineLatest(stickerPackSignals) + |> map { stickerPacks -> ([StickerPackCollectionInfo], TelegramMediaFile?) in + return (stickerPacks.compactMap { $0 }, firstCustomEmojiReaction) + } + } + |> deliverOnMainQueue).start(next: { [weak self] customEmojiPacks, firstCustomEmojiReaction in + guard let strongSelf = self else { + return + } + strongSelf.customEmojiPacks = customEmojiPacks + strongSelf.firstCustomEmojiReaction = firstCustomEmojiReaction + }) } if let currentStats = self.currentStats { @@ -2264,6 +2358,7 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus deinit { self.disposable?.dispose() + self.customEmojiPacksDisposable?.dispose() } override func didLoad() { @@ -2410,15 +2505,19 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus if let recentPeers = self.item.message.reactionsAttribute?.recentPeers, !recentPeers.isEmpty { for recentPeer in recentPeers { if let peer = self.item.message.peers[recentPeer.peerId] { - avatarsPeers.append(EnginePeer(peer)) - if avatarsPeers.count == 3 { - break + if !avatarsPeers.contains(where: { $0.id == peer.id }) { + avatarsPeers.append(EnginePeer(peer)) + if avatarsPeers.count == 3 { + break + } } } } } else if let peers = self.currentStats?.peers { for i in 0 ..< min(3, peers.count) { - avatarsPeers.append(peers[i]) + if !avatarsPeers.contains(where: { $0.id == peers[i].id }) { + avatarsPeers.append(peers[i]) + } } } avatarsContent = self.avatarsContext.update(peers: avatarsPeers, animated: false) @@ -2477,7 +2576,7 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus } self.item.action(controller, { [weak self] result in self?.actionSelected(result) - }, self.currentStats) + }, self.currentStats, self.customEmojiPacks, self.firstCustomEmojiReaction) } var isActionEnabled: Bool { diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 0cf3016e65..4da4fc9b83 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1061,7 +1061,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, @@ -1069,7 +1069,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.message) + canViewReactionList: canViewMessageReactionList(message: item.message), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -1214,7 +1216,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let reactions: ReactionsMessageAttribute - if shouldDisplayInlineDateReactions(message: item.message) { + if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) { reactions = ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: []) } else { reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: []) diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 87fa1c9184..2b761e18b0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -525,7 +525,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if let (media, flags) = mediaAndFlags { if let file = media as? TelegramMediaFile { if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if file.isInstantVideo { @@ -557,12 +557,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if file.isSticker || file.isAnimatedSticker { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else { @@ -608,7 +608,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } else if let image = media as? TelegramMediaImage { if !flags.contains(.preferMediaInline) { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { @@ -620,11 +620,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } else if let image = media as? TelegramMediaWebFile { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let wallpaper = media as? WallpaperPreviewMedia { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme { @@ -678,7 +678,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { impressionCount: viewCount, dateText: dateText, type: textStatusType, - layoutInput: .trailingContent(contentWidth: trailingContentWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), + layoutInput: .trailingContent(contentWidth: trailingContentWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), constrainedSize: textConstrainedSize, availableReactions: associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, @@ -686,7 +686,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: message) + canViewReactionList: canViewMessageReactionList(message: message), + animationCache: controllerInteraction.presentationContext.animationCache, + animationRenderer: controllerInteraction.presentationContext.animationRenderer )) } let _ = statusSuggestedWidthAndContinue diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 654afe9e8d..768a16c21b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -200,7 +200,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ let firstMessage = item.content.firstMessage - let reactionsAreInline = shouldDisplayInlineDateReactions(message: firstMessage) + let reactionsAreInline = shouldDisplayInlineDateReactions(message: firstMessage, isPremium: item.associatedData.isPremium) if reactionsAreInline { needReactions = false } @@ -1691,7 +1691,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, @@ -1699,7 +1699,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: message) + canViewReactionList: canViewMessageReactionList(message: message), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) mosaicStatusSizeAndApply = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index 31491aabfb..b496221ef6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -219,7 +219,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), + layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, @@ -227,7 +227,9 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.message) + canViewReactionList: canViewMessageReactionList(message: item.message), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index dda5bd45f3..b0101f7cb2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -10,6 +10,8 @@ import AccountContext import AppBundle import ReactionButtonListComponent import ReactionImageComponent +import AnimationCache +import MultiAnimationRenderer private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { if let _ = layer.animation(forKey: "clockFrameAnimation") { @@ -70,12 +72,12 @@ private final class StatusReactionNode: ASDisplayNode { self.fileDisposable?.dispose() } - func update(context: AccountContext, type: ChatMessageDateAndStatusType, value: MessageReaction.Reaction, file: TelegramMediaFile?, fileId: Int64?, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animated: Bool) { + func update(context: AccountContext, type: ChatMessageDateAndStatusType, value: MessageReaction.Reaction, file: TelegramMediaFile?, fileId: Int64?, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, animated: Bool) { if self.value != value { self.value = value let boundingImageSize = CGSize(width: 14.0, height: 14.0) - let defaultImageSize = CGSize(width: boundingImageSize.width + floor(boundingImageSize.width * 0.5 * 2.0), height: boundingImageSize.height + floor(boundingImageSize.height * 0.5 * 2.0)) + /*let defaultImageSize = CGSize(width: boundingImageSize.width + floor(boundingImageSize.width * 0.5 * 2.0), height: boundingImageSize.height + floor(boundingImageSize.height * 0.5 * 2.0)) let imageSize: CGSize if let file = file { self.iconImageDisposable.set((reactionStaticImage(context: context, animation: file, pixelSize: CGSize(width: 72.0, height: 72.0), queue: sharedReactionStaticImage) @@ -129,11 +131,31 @@ private final class StatusReactionNode: ASDisplayNode { self.fileDisposable?.dispose() self.fileDisposable = nil - } + }*/ - let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingImageSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((boundingImageSize.height - imageSize.height) / 2.0)), size: imageSize) + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingImageSize.width - boundingImageSize.width) / 2.0), y: floorToScreenPixels((boundingImageSize.height - boundingImageSize.height) / 2.0)), size: boundingImageSize) self.iconView.frame = iconFrame - self.iconView.update(size: iconFrame.size, transition: .immediate) + if let fileId = fileId ?? file?.fileId.id { + let animateIdle: Bool + if case .custom = value { + animateIdle = true + } else { + animateIdle = false + } + + self.iconView.update( + size: boundingImageSize, + context: context, + file: file, + fileId: fileId, + animationCache: animationCache, + animationRenderer: animationRenderer, + placeholderColor: .gray, + animateIdle: animateIdle, + reaction: value, + transition: .immediate + ) + } } } } @@ -192,6 +214,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var isPinned: Bool var hasAutoremove: Bool var canViewReactionList: Bool + var animationCache: AnimationCache + var animationRenderer: MultiAnimationRenderer init( context: AccountContext, @@ -208,7 +232,9 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { replyCount: Int, isPinned: Bool, hasAutoremove: Bool, - canViewReactionList: Bool + canViewReactionList: Bool, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer ) { self.context = context self.presentationData = presentationData @@ -225,6 +251,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { self.isPinned = isPinned self.hasAutoremove = hasAutoremove self.canViewReactionList = canViewReactionList + self.animationCache = animationCache + self.animationRenderer = animationRenderer } } @@ -711,16 +739,20 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { }, reactions: arguments.reactions.map { reaction in var centerAnimation: TelegramMediaFile? - var legacyIcon: TelegramMediaFile? + var animationFileId: Int64? - if let availableReactions = arguments.availableReactions { - for availableReaction in availableReactions.reactions { - if availableReaction.value == reaction.value { - centerAnimation = availableReaction.centerAnimation - legacyIcon = availableReaction.staticIcon - break + switch reaction.value { + case .builtin: + if let availableReactions = arguments.availableReactions { + for availableReaction in availableReactions.reactions { + if availableReaction.value == reaction.value { + centerAnimation = availableReaction.centerAnimation + break + } } } + case let .custom(fileId): + animationFileId = fileId } var peers: [EnginePeer] = [] @@ -737,11 +769,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { reaction: ReactionButtonComponent.Reaction( value: reaction.value, centerAnimation: centerAnimation, - legacyIcon: legacyIcon + animationFileId: animationFileId ), count: Int(reaction.count), peers: peers, - isSelected: reaction.isSelected + chosenOrder: reaction.chosenOrder ) }, colors: reactionColors, @@ -832,7 +864,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { strongSelf.type = arguments.type strongSelf.layoutSize = layoutSize - let reactionButtons = reactionButtonsResult.apply(animation) + let reactionButtons = reactionButtonsResult.apply( + animation, + ReactionButtonsAsyncLayoutContainer.Arguments( + animationCache: arguments.animationCache, + animationRenderer: arguments.animationRenderer + ) + ) var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset) for item in reactionButtons.items { @@ -1062,9 +1100,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { return false } } else { - return lhs.count > rhs.count + if let lhsIndex = lhs.chosenOrder, let rhsIndex = rhs.chosenOrder { + return lhsIndex < rhsIndex + } else { + return lhs.count > rhs.count + } } - }) { + }).prefix(10) { let node: StatusReactionNode var animateNode = true if let current = strongSelf.reactionNodes[reaction.value] { @@ -1093,7 +1135,20 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { reactionFileId = fileId } - node.update(context: arguments.context, type: arguments.type, value: reaction.value, file: centerAnimation, fileId: reactionFileId, isSelected: reaction.isSelected, count: Int(reaction.count), theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, animated: false) + node.update( + context: arguments.context, + type: arguments.type, + value: reaction.value, + file: centerAnimation, + fileId: reactionFileId, + isSelected: reaction.isSelected, + count: Int(reaction.count), + theme: arguments.presentationData.theme.theme, + wallpaper: arguments.presentationData.theme.wallpaper, + animationCache: arguments.animationCache, + animationRenderer: arguments.animationRenderer, + animated: false + ) if node.supernode == nil { strongSelf.addSubnode(node) if animation.isAnimated { @@ -1130,6 +1185,23 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { for id in removeIds { strongSelf.reactionNodes.removeValue(forKey: id) } + } else { + var removeIds: [MessageReaction.Reaction] = [] + for (id, node) in strongSelf.reactionNodes { + removeIds.append(id) + if animation.isAnimated { + node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in + node?.layer.removeAllAnimations() + node?.removeFromSupernode() + }) + } else { + node.removeFromSupernode() + } + } + for id in removeIds { + strongSelf.reactionNodes.removeValue(forKey: id) + } } if let (layout, apply) = reactionCountLayoutAndApply { @@ -1266,8 +1338,29 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } -func shouldDisplayInlineDateReactions(message: Message) -> Bool { +func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool) -> Bool { if message.id.peerId.namespace == Namespaces.Peer.CloudUser || message.id.peerId.namespace == Namespaces.Peer.SecretChat { + if let chatPeer = message.peers[message.id.peerId] as? TelegramUser, chatPeer.isPremium { + return false + } + if let author = message.author as? TelegramUser, author.isPremium { + return false + } + if isPremium { + return false + } + + if let effectiveReactions = message.effectiveReactions { + if effectiveReactions.count > 1 { + return false + } + for reaction in effectiveReactions { + if case .custom = reaction.value { + return false + } + } + } + return true } return false diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index e6751b0b0e..c10623445e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -543,7 +543,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } let reactions: ReactionsMessageAttribute - if shouldDisplayInlineDateReactions(message: item.message) { + if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) { reactions = ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: []) } else { reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: []) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 22fb75a9c9..3a1b7f8f7c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -747,7 +747,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let dateText = stringForMessageTimestampStatus(accountPeerId: arguments.context.account.peerId, message: arguments.message, dateTimeFormat: arguments.presentationData.dateTimeFormat, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, strings: arguments.presentationData.strings) - let displayReactionsInline = shouldDisplayInlineDateReactions(message: arguments.message) + let displayReactionsInline = shouldDisplayInlineDateReactions(message: arguments.message, isPremium: arguments.associatedData.isPremium) var reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings? if displayReactionsInline || arguments.displayReactions { @@ -776,7 +776,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { replyCount: dateReplies, isPinned: arguments.isPinned && !arguments.associatedData.isInPinnedListMode, hasAutoremove: arguments.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: arguments.message) + canViewReactionList: canViewMessageReactionList(message: arguments.message), + animationCache: arguments.controllerInteraction.presentationContext.animationCache, + animationRenderer: arguments.controllerInteraction.presentationContext.animationRenderer )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index 799220b1cd..9cdd15f6bf 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -325,7 +325,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, @@ -333,7 +333,9 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.message) + canViewReactionList: canViewMessageReactionList(message: item.message), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index d0b87b6282..0f69fd3f26 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -382,7 +382,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio }*/ } - func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { + func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let currentMessage = self.message let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() @@ -396,7 +396,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let currentAutomaticDownload = self.automaticDownload let currentAutomaticPlayback = self.automaticPlayback - return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in + return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode, presentationContext in var nativeSize: CGSize let isSecretMedia = message.containsSecretMedia @@ -512,7 +512,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio impressionCount: dateAndStatus.viewCount, dateText: dateAndStatus.dateText, type: dateAndStatus.type, - layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), availableReactions: associatedData.availableReactions, reactions: dateAndStatus.dateReactions, @@ -520,7 +520,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio replyCount: dateAndStatus.dateReplies, isPinned: dateAndStatus.isPinned, hasAutoremove: message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: message) + canViewReactionList: canViewMessageReactionList(message: message), + animationCache: presentationContext.animationCache, + animationRenderer: presentationContext.animationRenderer )) let (size, apply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -1549,12 +1551,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() - return { context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in + return { context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode, presentationContext in var imageNode: ChatMessageInteractiveMediaNode - var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) + var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node @@ -1564,7 +1566,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio imageLayout = imageNode.asyncLayout() } - let (unboundSize, initialWidth, continueLayout) = imageLayout(context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode) + let (unboundSize, initialWidth, continueLayout) = imageLayout(context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode, presentationContext) return (unboundSize, initialWidth, { constrainedSize, automaticPlayback, wideLayout, corners in let (finalWidth, finalLayout) = continueLayout(constrainedSize, automaticPlayback, wideLayout, corners) diff --git a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift index 2bde308c51..eccfce7563 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift @@ -252,7 +252,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, @@ -260,7 +260,9 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.message) + canViewReactionList: canViewMessageReactionList(message: item.message), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index 8ea5a05f53..78d742ed1e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -215,7 +215,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { ) } - let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.associatedData, item.attributes, selectedMedia!, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, sizeCalculation, layoutConstants, contentMode) + let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.associatedData, item.attributes, selectedMedia!, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, sizeCalculation, layoutConstants, contentMode, item.controllerInteraction.presentationContext) let forceFullCorners = false let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackground: .emptyWallpaper, forceFullCorners: forceFullCorners, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index f1b0920182..dddbec7a1a 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -1073,7 +1073,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), + layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), constrainedSize: textConstrainedSize, availableReactions: item.associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, @@ -1081,7 +1081,9 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.message) + canViewReactionList: canViewMessageReactionList(message: item.message), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index 78eca7ae28..69ef326a17 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -144,16 +144,20 @@ final class MessageReactionButtonsNode: ASDisplayNode { }, reactions: reactions.reactions.map { reaction in var centerAnimation: TelegramMediaFile? - var legacyIcon: TelegramMediaFile? + var animationFileId: Int64? - if let availableReactions = availableReactions { - for availableReaction in availableReactions.reactions { - if availableReaction.value == reaction.value { - centerAnimation = availableReaction.centerAnimation - legacyIcon = availableReaction.staticIcon - break + switch reaction.value { + case .builtin: + if let availableReactions = availableReactions { + for availableReaction in availableReactions.reactions { + if availableReaction.value == reaction.value { + centerAnimation = availableReaction.centerAnimation + break + } } } + case let .custom(fileId): + animationFileId = fileId } var peers: [EnginePeer] = [] @@ -176,11 +180,11 @@ final class MessageReactionButtonsNode: ASDisplayNode { reaction: ReactionButtonComponent.Reaction( value: reaction.value, centerAnimation: centerAnimation, - legacyIcon: legacyIcon + animationFileId: animationFileId ), count: Int(reaction.count), peers: peers, - isSelected: reaction.isSelected + chosenOrder: reaction.chosenOrder ) }, colors: reactionColors, @@ -266,7 +270,13 @@ final class MessageReactionButtonsNode: ASDisplayNode { reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset) } - let reactionButtons = reactionButtonsResult.apply(animation) + let reactionButtons = reactionButtonsResult.apply( + animation, + ReactionButtonsAsyncLayoutContainer.Arguments( + animationCache: presentationContext.animationCache, + animationRenderer: presentationContext.animationRenderer + ) + ) var validIds = Set() for item in reactionButtons.items { diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift index d7a7134c69..23953149d6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -120,7 +120,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: false)), + layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: item.associatedData.isPremium), preferAdditionalInset: false)), constrainedSize: textConstrainedSize, availableReactions: item.associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, @@ -128,7 +128,9 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.message) + canViewReactionList: canViewMessageReactionList(message: item.message), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 17934e5570..4fcb5d3b7b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -531,7 +531,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, @@ -539,7 +539,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.message) + canViewReactionList: canViewMessageReactionList(message: item.message), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -681,7 +683,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } let reactions: ReactionsMessageAttribute - if shouldDisplayInlineDateReactions(message: item.message) { + if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) { reactions = ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: []) } else { reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: []) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 25cadc1342..3008ce6a94 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -363,7 +363,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } let dateLayoutInput: ChatMessageDateAndStatusNode.LayoutInput - dateLayoutInput = .trailingContent(contentWidth: trailingWidthToMeasure, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: item.message), preferAdditionalInset: false)) + dateLayoutInput = .trailingContent(contentWidth: trailingWidthToMeasure, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium), preferAdditionalInset: false)) statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( context: item.context, @@ -380,7 +380,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, - canViewReactionList: canViewMessageReactionList(message: item.message) + canViewReactionList: canViewMessageReactionList(message: item.message), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) }