mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Message context menu
This commit is contained in:
parent
24834e428c
commit
e0c2800e8f
@ -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",
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user