[WIP] Message context menu

This commit is contained in:
Isaac 2024-04-30 23:40:55 +04:00
parent 24834e428c
commit e0c2800e8f
7 changed files with 423 additions and 77 deletions

View File

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

View File

@ -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<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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()
}

View File

@ -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<ReactionItem?, NoError>
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) {

View File

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

View File

@ -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<MessageReaction.Reaction>()
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
}
}

View File

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

View File

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