diff --git a/submodules/ChatSendMessageActionUI/BUILD b/submodules/ChatSendMessageActionUI/BUILD index c4a3e42239..aaf8818620 100644 --- a/submodules/ChatSendMessageActionUI/BUILD +++ b/submodules/ChatSendMessageActionUI/BUILD @@ -21,6 +21,9 @@ swift_library( "//submodules/TextFormat:TextFormat", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", + "//submodules/TelegramUI/Components/EntityKeyboard", + "//submodules/ReactionSelectionNode", + "//submodules/Components/ReactionButtonListComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index c36f64adf6..bac82431cd 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -8,6 +8,7 @@ import AccountContext import ContextUI import TelegramCore import TextFormat +import ReactionSelectionNode public final class ChatSendMessageActionSheetController: ViewController { public enum SendMode { @@ -34,6 +35,7 @@ public final class ChatSendMessageActionSheetController: ViewController { private let completion: () -> Void private let sendMessage: (SendMode) -> Void private let schedule: () -> Void + private let reactionItems: [ReactionItem]? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? @@ -46,7 +48,7 @@ public final class ChatSendMessageActionSheetController: ViewController { public var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, isScheduledMessages: Bool = false, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode) -> Void, schedule: @escaping () -> Void) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, isScheduledMessages: Bool = false, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode) -> Void, schedule: @escaping () -> Void, reactionItems: [ReactionItem]? = nil) { self.context = context self.peerId = peerId self.isScheduledMessages = isScheduledMessages @@ -60,6 +62,7 @@ public final class ChatSendMessageActionSheetController: ViewController { self.completion = completion self.sendMessage = sendMessage self.schedule = schedule + self.reactionItems = reactionItems self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } @@ -121,7 +124,7 @@ public final class ChatSendMessageActionSheetController: ViewController { self?.dismiss(cancel: false) }, cancel: { [weak self] in self?.dismiss(cancel: true) - }) + }, reactionItems: self.reactionItems) self.displayNodeDidLoad() } diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index 397c6657fd..4aa9d47e22 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -11,6 +11,9 @@ import ContextUI import TextFormat import EmojiTextAttachmentView import ChatInputTextNode +import ReactionSelectionNode +import EntityKeyboard +import ReactionButtonListComponent private let leftInset: CGFloat = 16.0 private let rightInset: CGFloat = 16.0 @@ -177,12 +180,17 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private let messageClipNode: ASDisplayNode private let messageBackgroundNode: ASImageNode + private let messageEffectAnchorView: UIView private let fromMessageTextScrollView: UIScrollView private let fromMessageTextNode: ChatInputTextNode private let toMessageTextScrollView: UIScrollView private let toMessageTextNode: ChatInputTextNode + private var messageEffectReactionIcon: ReactionIconView? private let scrollNode: ASScrollNode + private var selectedMessageEffect: (reaction: MessageReaction.Reaction, file: TelegramMediaFile)? + private var reactionContextNode: ReactionContextNode? + private var fromCustomEmojiContainerView: CustomEmojiContainerView? private var toCustomEmojiContainerView: CustomEmojiContainerView? @@ -196,7 +204,10 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? - init(context: AccountContext, presentationData: PresentationData, reminders: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, attachment: Bool, canSendWhenOnline: Bool, forwardedCount: Int?, hasEntityKeyboard: Bool, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, send: (() -> Void)?, sendSilently: (() -> Void)?, sendWhenOnline: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) { + private let messageEffectDisposable = MetaDisposable() + private var standaloneReactionAnimation: StandaloneReactionAnimation? + + init(context: AccountContext, presentationData: PresentationData, reminders: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, attachment: Bool, canSendWhenOnline: Bool, forwardedCount: Int?, hasEntityKeyboard: Bool, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, send: (() -> Void)?, sendSilently: (() -> Void)?, sendWhenOnline: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?, reactionItems: [ReactionItem]?) { self.context = context self.presentationData = presentationData self.sourceSendButton = sourceSendButton @@ -226,6 +237,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.messageClipNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) self.messageBackgroundNode = ASImageNode() self.messageBackgroundNode.isUserInteractionEnabled = true + self.messageEffectAnchorView = UIView() self.fromMessageTextNode = ChatInputTextNode(disableTiling: true) self.fromMessageTextNode.textView.isScrollEnabled = false self.fromMessageTextNode.isUserInteractionEnabled = false @@ -237,6 +249,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.toMessageTextScrollView = UIScrollView() self.toMessageTextScrollView.alpha = 0.0 self.toMessageTextScrollView.isUserInteractionEnabled = false + self.toMessageTextScrollView.clipsToBounds = false self.scrollNode = ASScrollNode() self.scrollNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) @@ -326,6 +339,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.addSubnode(self.sendButtonNode) self.scrollNode.addSubnode(self.messageClipNode) self.messageClipNode.addSubnode(self.messageBackgroundNode) + self.messageClipNode.view.addSubview(self.messageEffectAnchorView) self.messageClipNode.view.addSubview(self.fromMessageTextScrollView) self.fromMessageTextScrollView.addSubview(self.fromMessageTextNode.view) self.messageClipNode.view.addSubview(self.toMessageTextScrollView) @@ -364,6 +378,210 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } } } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + if let reactionItems, !reactionItems.isEmpty { + //TODO:localize + let reactionContextNode = ReactionContextNode( + context: context, + animationCache: context.animationCache, + presentationData: presentationData, + items: reactionItems.map(ReactionContextItem.reaction), + selectedItems: Set(), + title: "Add an animated effect", + reactionsLocked: false, + alwaysAllowPremiumReactions: false, + allPresetReactionsAreAvailable: true, + getEmojiContent: { animationCache, animationRenderer in + let mappedReactionItems: [EmojiComponentReactionItem] = reactionItems.map { reaction -> EmojiComponentReactionItem in + return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation) + } + + return EmojiPagerContentComponent.emojiInputData( + context: context, + animationCache: animationCache, + animationRenderer: animationRenderer, + isStandalone: false, + subject: .reaction(onlyTop: false), + hasTrending: false, + topReactionItems: mappedReactionItems, + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: context.account.peerId, + selectedItems: Set(), + hasSearch: false, + premiumIfSavedMessages: false + ) + }, + isExpandedUpdated: { [weak self] transition in + guard let self else { + return + } + self.update(transition: transition) + }, + requestLayout: { [weak self] transition in + guard let self else { + return + } + self.update(transition: transition) + }, + requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in + guard let self else { + return + } + self.update(transition: transition) + } + ) + reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in + guard let self, let reactionContextNode = self.reactionContextNode else { + return + } + + let reactionItem: Signal + switch updateReaction.reaction { + case .builtin: + reactionItem = context.engine.stickers.availableReactions() + |> take(1) + |> map { availableReactions -> ReactionItem? in + guard let availableReactions else { + return nil + } + for reaction in availableReactions.reactions { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + if reaction.value == updateReaction.reaction { + return ReactionItem( + reaction: ReactionItem.Reaction(rawValue: reaction.value), + appearAnimation: reaction.appearAnimation, + stillAnimation: reaction.selectAnimation, + listAnimation: centerAnimation, + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation, + isCustom: false + ) + } + } + return nil + } + case .custom: + switch updateReaction { + case let .custom(_, file): + if let itemFile = file { + reactionItem = .single(ReactionItem( + reaction: ReactionItem.Reaction(rawValue: updateReaction.reaction), + appearAnimation: itemFile, + stillAnimation: itemFile, + listAnimation: itemFile, + largeListAnimation: itemFile, + applicationAnimation: nil, + largeApplicationAnimation: nil, + isCustom: true + )) + } else { + reactionItem = .single(nil) + } + default: + reactionItem = .single(nil) + } + } + + self.messageEffectDisposable.set((combineLatest( + reactionItem, + ReactionContextNode.randomGenericReactionEffect(context: context) + ) + |> deliverOnMainQueue).startStrict(next: { [weak self] reactionItem, path in + guard let self else { + return + } + guard let reactionItem else { + return + } + + if let selectedMessageEffect = self.selectedMessageEffect { + if selectedMessageEffect.reaction == updateReaction.reaction { + self.selectedMessageEffect = nil + reactionContextNode.selectedItems = Set([]) + + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + self.standaloneReactionAnimation = nil + standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in + standaloneReactionAnimation?.removeFromSupernode() + }) + } + + self.update(transition: .animated(duration: 0.2, curve: .easeInOut)) + return + } else { + self.selectedMessageEffect = (reaction: updateReaction.reaction, file: reactionItem.listAnimation) + reactionContextNode.selectedItems = Set([AnyHashable(updateReaction.reaction)]) + self.update(transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } else { + self.selectedMessageEffect = (reaction: updateReaction.reaction, file: reactionItem.listAnimation) + reactionContextNode.selectedItems = Set([AnyHashable(updateReaction.reaction)]) + self.update(transition: .animated(duration: 0.2, curve: .easeInOut)) + } + + guard let messageEffectReactionIcon = self.messageEffectReactionIcon else { + return + } + + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + self.standaloneReactionAnimation = nil + standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in + standaloneReactionAnimation?.removeFromSupernode() + }) + } + + let genericReactionEffect = path + + let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: genericReactionEffect) + standaloneReactionAnimation.frame = self.bounds + self.standaloneReactionAnimation = standaloneReactionAnimation + self.addSubnode(standaloneReactionAnimation) + + standaloneReactionAnimation.animateReactionSelection( + context: context, + theme: self.presentationData.theme, + animationCache: context.animationCache, + reaction: reactionItem, + avatarPeers: [], + playHaptic: true, + isLarge: true, + playCenterReaction: false, + targetView: messageEffectReactionIcon, + addStandaloneReactionAnimation: { standaloneReactionAnimation in + /*guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)*/ + }, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) + })) + } + reactionContextNode.displayTail = true + reactionContextNode.forceTailToRight = false + reactionContextNode.forceDark = false + self.reactionContextNode = reactionContextNode + self.addSubnode(reactionContextNode) + } + + self.update(transition: .immediate) + } + + deinit { + self.messageEffectDisposable.dispose() } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -541,13 +759,17 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.fromMessageTextScrollView.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.toMessageTextScrollView.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY) + let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY) let springDuration: Double = 0.42 let springDamping: CGFloat = 104.0 self.contentContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 0.0, y: -clipDelta)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + } + Queue.mainQueue().after(0.01, { if self.animateInputField { self.textInputView.isHidden = true @@ -662,6 +884,19 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } self.fromMessageTextScrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) self.toMessageTextScrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -clipDelta), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + reactionContextNode.animateOut(to: nil, animatingOutToReaction: false) + } + + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + self.standaloneReactionAnimation = nil + standaloneReactionAnimation.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -clipDelta), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in + standaloneReactionAnimation?.removeFromSupernode() + }) + } } else { completedBubble = true } @@ -678,6 +913,12 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } } + private func update(transition: ContainedViewLayoutTransition) { + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout, transition: transition) + } + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout @@ -712,12 +953,12 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, if initialSendButtonFrame.width > initialSendButtonFrame.height * 1.2 { contentOrigin = CGPoint(x: layout.size.width - contentSize.width - layout.safeInsets.right - 5.0, y: initialSendButtonFrame.minY - contentSize.height) } else { - contentOrigin = CGPoint(x: layout.size.width - sideInset - contentSize.width - layout.safeInsets.right, y: layout.size.height - 6.0 - insets.bottom - contentSize.height) + contentOrigin = CGPoint(x: layout.size.width - sideInset - contentSize.width - layout.safeInsets.right, y: layout.size.height - 6.0 - insets.bottom - contentSize.height - 6.0) } if inputHeight > 70.0 && !layout.isNonExclusive && self.animateInputField { contentOrigin.y += menuHeightWithInset } - contentOrigin.y = min(contentOrigin.y + contentOffset, layout.size.height - 6.0 - layout.intrinsicInsets.bottom - contentSize.height) + contentOrigin.y = min(contentOrigin.y + contentOffset, layout.size.height - 6.0 - layout.intrinsicInsets.bottom - contentSize.height - 6.0) transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: contentOrigin, size: contentSize)) var nextY: CGFloat = 0.0 @@ -776,6 +1017,8 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, backgroundFrame.size.height += messageHeightAddition transition.updateFrame(node: self.messageBackgroundNode, frame: backgroundFrame) + transition.updateFrame(view: self.messageEffectAnchorView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 20.0, y: backgroundFrame.maxY - 10.0), size: CGSize(width: 1.0, height: 1.0))) + var textFrame = self.textFieldFrame textFrame.origin = CGPoint(x: 13.0, y: 6.0 - UIScreenPixel) textFrame.size.height = self.textInputView.contentSize.height @@ -803,6 +1046,53 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.toMessageTextScrollView.frame = textFrame self.toMessageTextNode.frame = CGRect(origin: CGPoint(), size: textFrame.size) self.toMessageTextNode.updateLayout(size: textFrame.size) + + if let selectedMessageEffect = self.selectedMessageEffect { + let messageEffectReactionIcon: ReactionIconView + var iconTransition = transition + var animateIn = false + if let current = self.messageEffectReactionIcon { + messageEffectReactionIcon = current + } else { + iconTransition = .immediate + animateIn = true + messageEffectReactionIcon = ReactionIconView(frame: CGRect()) + self.messageEffectReactionIcon = messageEffectReactionIcon + self.toMessageTextScrollView.addSubview(messageEffectReactionIcon) + } + + let iconSize = CGSize(width: 10.0, height: 10.0) + messageEffectReactionIcon.update(size: iconSize, context: self.context, file: selectedMessageEffect.file, fileId: selectedMessageEffect.file.fileId.id, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, tintColor: nil, placeholderColor: self.presentationData.theme.chat.message.stickerPlaceholderColor.withWallpaper, animateIdle: false, reaction: selectedMessageEffect.reaction, transition: iconTransition) + + let iconFrame = CGRect(origin: CGPoint(x: self.toMessageTextNode.frame.minX + messageFrame.width - 30.0 + 2.0 - iconSize.width, y: self.toMessageTextNode.frame.maxY - 6.0 - iconSize.height), size: iconSize) + iconTransition.updateFrame(view: messageEffectReactionIcon, frame: iconFrame) + if animateIn && transition.isAnimated { + transition.animateTransformScale(view: messageEffectReactionIcon, from: 0.001) + messageEffectReactionIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } else if let messageEffectReactionIcon = self.messageEffectReactionIcon { + self.messageEffectReactionIcon = nil + + transition.updateTransformScale(layer: messageEffectReactionIcon.layer, scale: 0.001) + transition.updateAlpha(layer: messageEffectReactionIcon.layer, alpha: 0.0, completion: { [weak messageEffectReactionIcon] _ in + messageEffectReactionIcon?.removeFromSuperview() + }) + } + + if let reactionContextNode = self.reactionContextNode { + let isFirstTime = reactionContextNode.bounds.isEmpty + + let size = layout.size + var reactionsAnchorRect = messageFrame + reactionsAnchorRect.origin.y = layout.size.height - reactionsAnchorRect.maxY + reactionsAnchorRect.origin.y -= 1.0 + transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + reactionContextNode.updateLayout(size: size, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: false, isCoveredByInput: false, isAnimatingOut: false, transition: transition) + reactionContextNode.updateIsIntersectingContent(isIntersectingContent: false, transition: .immediate) + if isFirstTime { + reactionContextNode.animateIn(from: reactionsAnchorRect) + } + } } @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index d413abc2b8..753c2ead47 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -2966,13 +2966,13 @@ public final class StandaloneReactionAnimation: ASDisplayNode { self.isUserInteractionEnabled = false } - public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, hideCenterAnimation: 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, hideCenterAnimation: hideCenterAnimation, 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, playCenterReaction: Bool = true, forceSmallEffectAnimation: Bool = false, hideCenterAnimation: 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, playCenterReaction: playCenterReaction, forceSmallEffectAnimation: forceSmallEffectAnimation, hideCenterAnimation: hideCenterAnimation, targetView: targetView, addStandaloneReactionAnimation: addStandaloneReactionAnimation, currentItemNode: nil, completion: completion) } public var currentDismissAnimation: (() -> Void)? - public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, hideCenterAnimation: 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, playCenterReaction: Bool = true, forceSmallEffectAnimation: Bool = false, hideCenterAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, currentItemNode: ReactionNode?, completion: @escaping () -> Void) { guard let sourceSnapshotView = targetView.snapshotContentTree() else { completion() return @@ -2984,28 +2984,36 @@ public final class StandaloneReactionAnimation: ASDisplayNode { self.targetView = targetView - let itemNode: ReactionNode - if let currentItemNode = currentItemNode { - itemNode = currentItemNode + let itemNode: ReactionNode? + if playCenterReaction { + if let currentItemNode = currentItemNode { + itemNode = currentItemNode + } else { + let animationRenderer = MultiAnimationRendererImpl() + itemNode = ReactionNode(context: context, theme: theme, item: reaction, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false) + } + self.itemNode = itemNode } else { - let animationRenderer = MultiAnimationRendererImpl() - itemNode = ReactionNode(context: context, theme: theme, item: reaction, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false) + itemNode = nil } - self.itemNode = itemNode let switchToInlineImmediately: Bool - if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji { - switch itemNode.item.reaction.rawValue { - case .builtin: + if let itemNode { + if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji { + switch itemNode.item.reaction.rawValue { + case .builtin: + switchToInlineImmediately = false + case .custom: + switchToInlineImmediately = true + } + } else { switchToInlineImmediately = false - case .custom: - switchToInlineImmediately = true } } else { switchToInlineImmediately = false } - if !forceSmallEffectAnimation && !switchToInlineImmediately && !hideCenterAnimation { + if let itemNode, !forceSmallEffectAnimation, !switchToInlineImmediately, !hideCenterAnimation { if let targetView = targetView as? ReactionIconView, !isLarge { self.itemNodeIsEmbedded = true targetView.addSubnode(itemNode) @@ -3014,28 +3022,27 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } } - itemNode.expandedAnimationDidBegin = { [weak self, weak targetView] in - guard let strongSelf = self, let targetView = targetView else { - return - } - if let targetView = targetView as? ReactionIconView, !isLarge { - strongSelf.itemNodeIsEmbedded = true - - targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) - } else { - targetView.isHidden = true + if let itemNode { + itemNode.expandedAnimationDidBegin = { [weak self, weak targetView] in + guard let strongSelf = self, let targetView = targetView else { + return + } + if let targetView = targetView as? ReactionIconView, !isLarge { + strongSelf.itemNodeIsEmbedded = true + + targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) + } else { + targetView.isHidden = true + } } + + itemNode.isExtracted = true } - - itemNode.isExtracted = true var selfTargetBounds = targetView.bounds if let targetView = targetView as? ReactionIconView, let iconFrame = targetView.iconFrame { selfTargetBounds = iconFrame } - /*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) @@ -3064,21 +3071,23 @@ public final class StandaloneReactionAnimation: ASDisplayNode { }) } - if self.itemNodeIsEmbedded { - itemNode.frame = selfTargetBounds - } else { - itemNode.frame = expandedFrame + if let itemNode { + if self.itemNodeIsEmbedded { + itemNode.frame = selfTargetBounds + } else { + itemNode.frame = expandedFrame + + itemNode.layer.animateSpring(from: (selfTargetRect.width / expandedFrame.width) as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.7) + } - itemNode.layer.animateSpring(from: (selfTargetRect.width / expandedFrame.width) as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.7) + itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, largeExpanded: isLarge, isPreviewing: false, transition: .immediate) } - itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, largeExpanded: isLarge, isPreviewing: false, transition: .immediate) - let additionalAnimation: TelegramMediaFile? if isLarge && !forceSmallEffectAnimation { - additionalAnimation = itemNode.item.largeApplicationAnimation + additionalAnimation = reaction.largeApplicationAnimation } else { - additionalAnimation = itemNode.item.applicationAnimation + additionalAnimation = reaction.applicationAnimation } let additionalAnimationNode: AnimatedStickerNode? @@ -3099,15 +3108,13 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } } - var additionalCachePathPrefix: String? - additionalCachePathPrefix = itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(additionalAnimation.resource.id) - additionalCachePathPrefix = nil + let additionalCachePathPrefix: String? = nil - additionalAnimationNodeValue.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: additionalAnimation.resource), width: Int(effectFrame.width * 1.33), height: Int(effectFrame.height * 1.33), playbackMode: .once, mode: .direct(cachePathPrefix: additionalCachePathPrefix)) + additionalAnimationNodeValue.setup(source: AnimatedStickerResourceSource(account: context.account, resource: additionalAnimation.resource), width: Int(effectFrame.width * 1.33), height: Int(effectFrame.height * 1.33), playbackMode: .once, mode: .direct(cachePathPrefix: additionalCachePathPrefix)) additionalAnimationNodeValue.frame = effectFrame additionalAnimationNodeValue.updateLayout(size: effectFrame.size) self.addSubnode(additionalAnimationNodeValue) - } else if itemNode.item.isCustom { + } else if reaction.isCustom { var effectURL: URL? if let genericReactionEffect = self.genericReactionEffect { effectURL = URL(fileURLWithPath: genericReactionEffect) @@ -3157,18 +3164,18 @@ public final class StandaloneReactionAnimation: ASDisplayNode { genericAnimationView = view - let animationCache = itemNode.context.animationCache - let animationRenderer = itemNode.context.animationRenderer + let animationCache = context.animationCache + let animationRenderer = context.animationRenderer for i in 1 ... 7 { let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "placeholder_\(i)")) for animationLayer in allLayers { let baseItemLayer = InlineStickerItemLayer( - context: itemNode.context, + context: context, userLocation: .other, attemptSynchronousLoad: false, - emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: itemNode.item.listAnimation.fileId.id, file: itemNode.item.listAnimation), - file: itemNode.item.listAnimation, + emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: reaction.listAnimation.fileId.id, file: reaction.listAnimation), + file: reaction.listAnimation, cache: animationCache, renderer: animationRenderer, placeholderColor: UIColor(white: 0.0, alpha: 0.0), @@ -3256,15 +3263,6 @@ public final class StandaloneReactionAnimation: ASDisplayNode { return } - /*if switchToInlineImmediately { - targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) - itemNode.isHidden = true - } else { - targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) - targetView.addSubnode(itemNode) - itemNode.frame = selfTargetBounds - }*/ - if forceSmallEffectAnimation { if let additionalAnimationNode = additionalAnimationNode { additionalAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak additionalAnimationNode] _ in @@ -3275,7 +3273,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { mainAnimationCompleted = true intermediateCompletion() } else { - if isLarge { + if isLarge, let itemNode { let genericReactionEffect = strongSelf.genericReactionEffect strongSelf.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: true, completion: { if let addStandaloneReactionAnimation = addStandaloneReactionAnimation { @@ -3284,10 +3282,10 @@ public final class StandaloneReactionAnimation: ASDisplayNode { addStandaloneReactionAnimation(standaloneReactionAnimation) standaloneReactionAnimation.animateReactionSelection( - context: itemNode.context, - theme: itemNode.context.sharedContext.currentPresentationData.with({ $0 }).theme, + context: context, + theme: context.sharedContext.currentPresentationData.with({ $0 }).theme, animationCache: animationCache, - reaction: itemNode.item, + reaction: reaction, avatarPeers: avatarPeers, playHaptic: false, isLarge: false, @@ -3331,10 +3329,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } if forceSmallEffectAnimation { - //itemNode.mainAnimationCompletion = { - mainAnimationCompleted = true - maybeBeginDismissAnimation() - //} + mainAnimationCompleted = true + maybeBeginDismissAnimation() } if let additionalAnimationNode = additionalAnimationNode { diff --git a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift index 8bd9be9f67..5ab5f01380 100644 --- a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift +++ b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift @@ -419,3 +419,45 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe return result } } + +public func effectMessageReactions(context: AccountContext) -> Signal<[ReactionItem], NoError> { + return context.engine.stickers.availableReactions() + |> take(1) + |> map { availableReactions -> [ReactionItem] in + guard let availableReactions else { + return [] + } + + var result: [ReactionItem] = [] + var existingIds = Set() + + for reaction in availableReactions.reactions { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + if !reaction.isEnabled { + continue + } + if existingIds.contains(reaction.value) { + continue + } + existingIds.insert(reaction.value) + + result.append(ReactionItem( + reaction: ReactionItem.Reaction(rawValue: reaction.value), + appearAnimation: reaction.appearAnimation, + stillAnimation: reaction.selectAnimation, + listAnimation: centerAnimation, + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation, + isCustom: false + )) + } + + return result + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 38210a39b0..a1eee5093b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1219,7 +1219,7 @@ extension ChatControllerImpl { let transformedMessages: [EnqueueMessage] if let silentPosting = silentPosting { - transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting) + transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting, scheduleTime: scheduleTime) } else if let scheduleTime = scheduleTime { transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) } else { diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift index 50639205c6..5084606da5 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift @@ -9,6 +9,8 @@ import TelegramCore import TelegramNotices import ChatSendMessageActionUI import AccountContext +import TopMessageReactions +import ReactionSelectionNode func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, node: ASDisplayNode, gesture: ContextGesture) { guard let peerId = selfController.chatLocation.peerId, let textInputView = selfController.chatDisplayNode.textInputView(), let layout = selfController.validLayout else { @@ -28,9 +30,19 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no hasEntityKeyboard = true } - let _ = (selfController.context.account.viewTracker.peerView(peerId) - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView in + let effectItems: Signal<[ReactionItem]?, NoError> + if peerId != selfController.context.account.peerId && peerId.namespace == Namespaces.Peer.CloudUser { + effectItems = effectMessageReactions(context: selfController.context) + |> map(Optional.init) + } else { + effectItems = .single(nil) + } + + let _ = (combineLatest( + selfController.context.account.viewTracker.peerView(peerId) |> take(1), + effectItems + ) + |> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView, effectItems in guard let selfController, let peer = peerViewMainPeer(peerView) else { return } @@ -79,7 +91,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no return } selfController.controllerInteraction?.scheduleCurrentMessage() - }) + }, reactionItems: effectItems) controller.emojiViewProvider = selfController.chatDisplayNode.textInputPanelNode?.emojiViewProvider selfController.sendMessageActionsController = controller if layout.isNonExclusive {