mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
[WIP] Send message effects
This commit is contained in:
@@ -14,6 +14,7 @@ swift_library(
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
@@ -24,6 +25,13 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard",
|
||||
"//submodules/ReactionSelectionNode",
|
||||
"//submodules/Components/ReactionButtonListComponent",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/ChatMessageBackground",
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -9,13 +9,28 @@ import ContextUI
|
||||
import TelegramCore
|
||||
import TextFormat
|
||||
import ReactionSelectionNode
|
||||
import WallpaperBackgroundNode
|
||||
|
||||
public final class ChatSendMessageActionSheetController: ViewController {
|
||||
public enum SendMode {
|
||||
case generic
|
||||
case silently
|
||||
case whenOnline
|
||||
public enum ChatSendMessageActionSheetControllerSendMode {
|
||||
case generic
|
||||
case silently
|
||||
case whenOnline
|
||||
}
|
||||
|
||||
public final class ChatSendMessageActionSheetControllerMessageEffect {
|
||||
public let id: Int64
|
||||
|
||||
public init(id: Int64) {
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ChatSendMessageActionSheetController: ViewController {
|
||||
typealias SendMode = ChatSendMessageActionSheetControllerSendMode
|
||||
typealias MessageEffect = ChatSendMessageActionSheetControllerMessageEffect
|
||||
}
|
||||
|
||||
private final class ChatSendMessageActionSheetControllerImpl: ViewController, ChatSendMessageActionSheetController {
|
||||
private var controllerNode: ChatSendMessageActionSheetControllerNode {
|
||||
return self.displayNode as! ChatSendMessageActionSheetControllerNode
|
||||
}
|
||||
@@ -33,8 +48,8 @@ public final class ChatSendMessageActionSheetController: ViewController {
|
||||
private let attachment: Bool
|
||||
private let canSendWhenOnline: Bool
|
||||
private let completion: () -> Void
|
||||
private let sendMessage: (SendMode) -> Void
|
||||
private let schedule: () -> Void
|
||||
private let sendMessage: (SendMode, MessageEffect?) -> Void
|
||||
private let schedule: (MessageEffect?) -> Void
|
||||
private let reactionItems: [ReactionItem]?
|
||||
|
||||
private var presentationData: PresentationData
|
||||
@@ -46,9 +61,9 @@ public final class ChatSendMessageActionSheetController: ViewController {
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
public var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||
private let 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, reactionItems: [ReactionItem]? = nil) {
|
||||
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, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode, MessageEffect?) -> Void, schedule: @escaping (MessageEffect?) -> Void, reactionItems: [ReactionItem]? = nil) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.isScheduledMessages = isScheduledMessages
|
||||
@@ -57,6 +72,7 @@ public final class ChatSendMessageActionSheetController: ViewController {
|
||||
self.gesture = gesture
|
||||
self.sourceSendButton = sourceSendButton
|
||||
self.textInputView = textInputView
|
||||
self.emojiViewProvider = emojiViewProvider
|
||||
self.attachment = attachment
|
||||
self.canSendWhenOnline = canSendWhenOnline
|
||||
self.completion = completion
|
||||
@@ -111,16 +127,32 @@ public final class ChatSendMessageActionSheetController: ViewController {
|
||||
}
|
||||
|
||||
self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, presentationData: self.presentationData, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputView: self.textInputView, attachment: self.attachment, canSendWhenOnline: self.canSendWhenOnline, forwardedCount: forwardedCount, hasEntityKeyboard: self.hasEntityKeyboard, emojiViewProvider: self.emojiViewProvider, send: { [weak self] in
|
||||
self?.sendMessage(.generic)
|
||||
var messageEffect: MessageEffect?
|
||||
if let selectedEffect = self?.controllerNode.selectedMessageEffect {
|
||||
messageEffect = MessageEffect(id: selectedEffect.id)
|
||||
}
|
||||
self?.sendMessage(.generic, messageEffect)
|
||||
self?.dismiss(cancel: false)
|
||||
}, sendSilently: { [weak self] in
|
||||
self?.sendMessage(.silently)
|
||||
var messageEffect: MessageEffect?
|
||||
if let selectedEffect = self?.controllerNode.selectedMessageEffect {
|
||||
messageEffect = MessageEffect(id: selectedEffect.id)
|
||||
}
|
||||
self?.sendMessage(.silently, messageEffect)
|
||||
self?.dismiss(cancel: false)
|
||||
}, sendWhenOnline: { [weak self] in
|
||||
self?.sendMessage(.whenOnline)
|
||||
var messageEffect: MessageEffect?
|
||||
if let selectedEffect = self?.controllerNode.selectedMessageEffect {
|
||||
messageEffect = MessageEffect(id: selectedEffect.id)
|
||||
}
|
||||
self?.sendMessage(.whenOnline, messageEffect)
|
||||
self?.dismiss(cancel: false)
|
||||
}, schedule: !canSchedule ? nil : { [weak self] in
|
||||
self?.schedule()
|
||||
var messageEffect: MessageEffect?
|
||||
if let selectedEffect = self?.controllerNode.selectedMessageEffect {
|
||||
messageEffect = MessageEffect(id: selectedEffect.id)
|
||||
}
|
||||
self?.schedule(messageEffect)
|
||||
self?.dismiss(cancel: false)
|
||||
}, cancel: { [weak self] in
|
||||
self?.dismiss(cancel: true)
|
||||
@@ -160,3 +192,64 @@ public final class ChatSendMessageActionSheetController: ViewController {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func makeChatSendMessageActionSheetController(
|
||||
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,
|
||||
emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?,
|
||||
wallpaperBackgroundNode: WallpaperBackgroundNode? = nil,
|
||||
attachment: Bool = false,
|
||||
canSendWhenOnline: Bool,
|
||||
completion: @escaping () -> Void,
|
||||
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
reactionItems: [ReactionItem]? = nil
|
||||
) -> ChatSendMessageActionSheetController {
|
||||
if textInputView.text.isEmpty {
|
||||
return ChatSendMessageActionSheetControllerImpl(
|
||||
context: context,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
peerId: peerId,
|
||||
isScheduledMessages: isScheduledMessages,
|
||||
forwardMessageIds: forwardMessageIds,
|
||||
hasEntityKeyboard: hasEntityKeyboard,
|
||||
gesture: gesture,
|
||||
sourceSendButton: sourceSendButton,
|
||||
textInputView: textInputView,
|
||||
emojiViewProvider: emojiViewProvider,
|
||||
attachment: attachment,
|
||||
canSendWhenOnline: canSendWhenOnline,
|
||||
completion: completion,
|
||||
sendMessage: sendMessage,
|
||||
schedule: schedule,
|
||||
reactionItems: reactionItems
|
||||
)
|
||||
}
|
||||
|
||||
return ChatSendMessageContextScreen(
|
||||
context: context,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
peerId: peerId,
|
||||
isScheduledMessages: isScheduledMessages,
|
||||
forwardMessageIds: forwardMessageIds,
|
||||
hasEntityKeyboard: hasEntityKeyboard,
|
||||
gesture: gesture,
|
||||
sourceSendButton: sourceSendButton,
|
||||
textInputView: textInputView,
|
||||
emojiViewProvider: emojiViewProvider,
|
||||
wallpaperBackgroundNode: wallpaperBackgroundNode,
|
||||
attachment: attachment,
|
||||
canSendWhenOnline: canSendWhenOnline,
|
||||
completion: completion,
|
||||
sendMessage: sendMessage,
|
||||
schedule: schedule,
|
||||
reactionItems: reactionItems
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
@@ -186,9 +187,10 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
private let toMessageTextScrollView: UIScrollView
|
||||
private let toMessageTextNode: ChatInputTextNode
|
||||
private var messageEffectReactionIcon: ReactionIconView?
|
||||
private var messageEffectReactionText: ImmediateTextView?
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
private var selectedMessageEffect: (reaction: MessageReaction.Reaction, file: TelegramMediaFile)?
|
||||
private(set) var selectedMessageEffect: (id: Int64, effect: AvailableMessageEffects.MessageEffect)?
|
||||
private var reactionContextNode: ReactionContextNode?
|
||||
|
||||
private var fromCustomEmojiContainerView: CustomEmojiContainerView?
|
||||
@@ -380,7 +382,6 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
if let reactionItems, !reactionItems.isEmpty {
|
||||
//TODO:localize
|
||||
let reactionContextNode = ReactionContextNode(
|
||||
@@ -394,24 +395,12 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
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(
|
||||
return EmojiPagerContentComponent.messageEffectsInputData(
|
||||
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
|
||||
hasSearch: true,
|
||||
hideBackground: false
|
||||
)
|
||||
},
|
||||
isExpandedUpdated: { [weak self] transition in
|
||||
@@ -438,73 +427,51 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
guard case let .custom(sourceEffectId, _) = updateReaction else {
|
||||
return
|
||||
}
|
||||
|
||||
let messageEffect: Signal<AvailableMessageEffects.MessageEffect?, NoError>
|
||||
messageEffect = context.engine.stickers.availableMessageEffects()
|
||||
|> take(1)
|
||||
|> map { availableMessageEffects -> AvailableMessageEffects.MessageEffect? in
|
||||
guard let availableMessageEffects else {
|
||||
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)
|
||||
for messageEffect in availableMessageEffects.messageEffects {
|
||||
if messageEffect.id == sourceEffectId || messageEffect.effectSticker.fileId.id == sourceEffectId {
|
||||
return messageEffect
|
||||
}
|
||||
default:
|
||||
reactionItem = .single(nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
self.messageEffectDisposable.set((combineLatest(
|
||||
reactionItem,
|
||||
messageEffect,
|
||||
ReactionContextNode.randomGenericReactionEffect(context: context)
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] reactionItem, path in
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect, path in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let reactionItem else {
|
||||
guard let messageEffect else {
|
||||
return
|
||||
}
|
||||
let effectId = messageEffect.id
|
||||
|
||||
let reactionItem = ReactionItem(
|
||||
reaction: ReactionItem.Reaction(rawValue: updateReaction.reaction),
|
||||
appearAnimation: messageEffect.effectSticker,
|
||||
stillAnimation: messageEffect.effectSticker,
|
||||
listAnimation: messageEffect.effectSticker,
|
||||
largeListAnimation: messageEffect.effectSticker,
|
||||
applicationAnimation: nil,
|
||||
largeApplicationAnimation: nil,
|
||||
isCustom: true
|
||||
)
|
||||
|
||||
if let selectedMessageEffect = self.selectedMessageEffect {
|
||||
if selectedMessageEffect.reaction == updateReaction.reaction {
|
||||
if selectedMessageEffect.id == effectId {
|
||||
self.selectedMessageEffect = nil
|
||||
reactionContextNode.selectedItems = Set([])
|
||||
|
||||
@@ -518,17 +485,17 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
self.update(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
return
|
||||
} else {
|
||||
self.selectedMessageEffect = (reaction: updateReaction.reaction, file: reactionItem.listAnimation)
|
||||
self.selectedMessageEffect = (id: effectId, effect: messageEffect)
|
||||
reactionContextNode.selectedItems = Set([AnyHashable(updateReaction.reaction)])
|
||||
self.update(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
} else {
|
||||
self.selectedMessageEffect = (reaction: updateReaction.reaction, file: reactionItem.listAnimation)
|
||||
self.selectedMessageEffect = (id: effectId, effect: messageEffect)
|
||||
reactionContextNode.selectedItems = Set([AnyHashable(updateReaction.reaction)])
|
||||
self.update(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
|
||||
guard let messageEffectReactionIcon = self.messageEffectReactionIcon else {
|
||||
guard let targetView = self.messageEffectReactionIcon ?? self.messageEffectReactionText else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -546,16 +513,27 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
self.standaloneReactionAnimation = standaloneReactionAnimation
|
||||
self.addSubnode(standaloneReactionAnimation)
|
||||
|
||||
var customEffectResource: MediaResource?
|
||||
if let effectAnimation = messageEffect.effectAnimation {
|
||||
customEffectResource = effectAnimation.resource
|
||||
} else {
|
||||
let effectSticker = messageEffect.effectSticker
|
||||
if let effectFile = effectSticker.videoThumbnails.first {
|
||||
customEffectResource = effectFile.resource
|
||||
}
|
||||
}
|
||||
|
||||
standaloneReactionAnimation.animateReactionSelection(
|
||||
context: context,
|
||||
theme: self.presentationData.theme,
|
||||
animationCache: context.animationCache,
|
||||
reaction: reactionItem,
|
||||
customEffectResource: customEffectResource,
|
||||
avatarPeers: [],
|
||||
playHaptic: true,
|
||||
isLarge: true,
|
||||
playCenterReaction: false,
|
||||
targetView: messageEffectReactionIcon,
|
||||
targetView: targetView,
|
||||
addStandaloneReactionAnimation: { standaloneReactionAnimation in
|
||||
/*guard let strongSelf = self else {
|
||||
return
|
||||
@@ -899,6 +877,9 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
}
|
||||
} else {
|
||||
completedBubble = true
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
reactionContextNode.animateOut(to: nil, animatingOutToReaction: false)
|
||||
}
|
||||
}
|
||||
|
||||
let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY)
|
||||
@@ -1048,35 +1029,89 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
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
|
||||
if let iconFile = selectedMessageEffect.effect.staticIcon {
|
||||
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: iconFile, fileId: iconFile.fileId.id, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, tintColor: nil, placeholderColor: self.presentationData.theme.chat.message.stickerPlaceholderColor.withWallpaper, animateIdle: false, reaction: .custom(selectedMessageEffect.id), 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)
|
||||
}
|
||||
|
||||
if let messageEffectReactionText = self.messageEffectReactionText {
|
||||
self.messageEffectReactionText = nil
|
||||
|
||||
transition.updateTransformScale(layer: messageEffectReactionText.layer, scale: 0.001)
|
||||
transition.updateAlpha(layer: messageEffectReactionText.layer, alpha: 0.0, completion: { [weak messageEffectReactionText] _ in
|
||||
messageEffectReactionText?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
iconTransition = .immediate
|
||||
animateIn = true
|
||||
messageEffectReactionIcon = ReactionIconView(frame: CGRect())
|
||||
self.messageEffectReactionIcon = messageEffectReactionIcon
|
||||
self.toMessageTextScrollView.addSubview(messageEffectReactionIcon)
|
||||
let messageEffectReactionText: ImmediateTextView
|
||||
var iconTransition = transition
|
||||
var animateIn = false
|
||||
if let current = self.messageEffectReactionText {
|
||||
messageEffectReactionText = current
|
||||
} else {
|
||||
iconTransition = .immediate
|
||||
animateIn = true
|
||||
messageEffectReactionText = ImmediateTextView()
|
||||
self.messageEffectReactionText = messageEffectReactionText
|
||||
self.toMessageTextScrollView.addSubview(messageEffectReactionText)
|
||||
}
|
||||
|
||||
let iconSize = CGSize(width: 10.0, height: 10.0)
|
||||
messageEffectReactionText.attributedText = NSAttributedString(string: selectedMessageEffect.effect.emoticon, font: Font.regular(9.0), textColor: .black)
|
||||
let textSize = messageEffectReactionText.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: self.toMessageTextNode.frame.minX + messageFrame.width - 30.0 + 2.0 - iconSize.width + floorToScreenPixels((iconSize.width - textSize.width) * 0.5), y: self.toMessageTextNode.frame.maxY - 6.0 - iconSize.height + floorToScreenPixels((iconSize.height - textSize.height) * 0.5)), size: textSize)
|
||||
iconTransition.updateFrame(view: messageEffectReactionText, frame: iconFrame)
|
||||
if animateIn && transition.isAnimated {
|
||||
transition.animateTransformScale(view: messageEffectReactionText, from: 0.001)
|
||||
messageEffectReactionText.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 messageEffectReactionText = self.messageEffectReactionText {
|
||||
self.messageEffectReactionText = nil
|
||||
|
||||
transition.updateTransformScale(layer: messageEffectReactionText.layer, scale: 0.001)
|
||||
transition.updateAlpha(layer: messageEffectReactionText.layer, alpha: 0.0, completion: { [weak messageEffectReactionText] _ in
|
||||
messageEffectReactionText?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
} 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 {
|
||||
|
||||
@@ -0,0 +1,860 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import TextFormat
|
||||
import ReactionSelectionNode
|
||||
import ViewControllerComponent
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import WallpaperBackgroundNode
|
||||
import ReactionSelectionNode
|
||||
import EntityKeyboard
|
||||
|
||||
func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
|
||||
let sourceWindowFrame = fromView.convert(frame, to: nil)
|
||||
var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil)
|
||||
|
||||
if let fromWindow = fromView.window, let toWindow = toView.window {
|
||||
targetWindowFrame.origin.x += toWindow.bounds.width - fromWindow.bounds.width
|
||||
}
|
||||
return targetWindowFrame
|
||||
}
|
||||
|
||||
final class ChatSendMessageContextScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let peerId: EnginePeer.Id?
|
||||
let isScheduledMessages: Bool
|
||||
let forwardMessageIds: [EngineMessage.Id]?
|
||||
let hasEntityKeyboard: Bool
|
||||
let gesture: ContextGesture
|
||||
let sourceSendButton: ASDisplayNode
|
||||
let textInputView: UITextView
|
||||
let emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||
let wallpaperBackgroundNode: WallpaperBackgroundNode?
|
||||
let attachment: Bool
|
||||
let canSendWhenOnline: Bool
|
||||
let completion: () -> Void
|
||||
let sendMessage: (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void
|
||||
let schedule: (ChatSendMessageActionSheetController.MessageEffect?) -> Void
|
||||
let reactionItems: [ReactionItem]?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id?,
|
||||
isScheduledMessages: Bool,
|
||||
forwardMessageIds: [EngineMessage.Id]?,
|
||||
hasEntityKeyboard: Bool,
|
||||
gesture: ContextGesture,
|
||||
sourceSendButton: ASDisplayNode,
|
||||
textInputView: UITextView,
|
||||
emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?,
|
||||
wallpaperBackgroundNode: WallpaperBackgroundNode?,
|
||||
attachment: Bool,
|
||||
canSendWhenOnline: Bool,
|
||||
completion: @escaping () -> Void,
|
||||
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
reactionItems: [ReactionItem]?
|
||||
) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.isScheduledMessages = isScheduledMessages
|
||||
self.forwardMessageIds = forwardMessageIds
|
||||
self.hasEntityKeyboard = hasEntityKeyboard
|
||||
self.gesture = gesture
|
||||
self.sourceSendButton = sourceSendButton
|
||||
self.textInputView = textInputView
|
||||
self.emojiViewProvider = emojiViewProvider
|
||||
self.wallpaperBackgroundNode = wallpaperBackgroundNode
|
||||
self.attachment = attachment
|
||||
self.canSendWhenOnline = canSendWhenOnline
|
||||
self.completion = completion
|
||||
self.sendMessage = sendMessage
|
||||
self.schedule = schedule
|
||||
self.reactionItems = reactionItems
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatSendMessageContextScreenComponent, rhs: ChatSendMessageContextScreenComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
enum PresentationAnimationState {
|
||||
enum Key {
|
||||
case initial
|
||||
case animatedIn
|
||||
case animatedOut
|
||||
}
|
||||
|
||||
case initial
|
||||
case animatedIn
|
||||
case animatedOut(completion: () -> Void)
|
||||
|
||||
var key: Key {
|
||||
switch self {
|
||||
case .initial:
|
||||
return .initial
|
||||
case .animatedIn:
|
||||
return .animatedIn
|
||||
case .animatedOut:
|
||||
return .animatedOut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
|
||||
private var sendButton: HighlightTrackingButton?
|
||||
private var messageItemView: MessageItemView?
|
||||
private var actionsStackNode: ContextControllerActionsStackNode?
|
||||
private var reactionContextNode: ReactionContextNode?
|
||||
|
||||
private let scrollView: UIScrollView
|
||||
|
||||
private var component: ChatSendMessageContextScreenComponent?
|
||||
private var environment: EnvironmentType?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private let messageEffectDisposable = MetaDisposable()
|
||||
private var selectedMessageEffect: AvailableMessageEffects.MessageEffect?
|
||||
private var standaloneReactionAnimation: StandaloneReactionAnimation?
|
||||
|
||||
private var presentationAnimationState: PresentationAnimationState = .initial
|
||||
private var appliedAnimationState: PresentationAnimationState = .initial
|
||||
private var animateOutToEmpty: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
|
||||
self.scrollView = UIScrollView()
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onBackgroundTap(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.messageEffectDisposable.dispose()
|
||||
}
|
||||
|
||||
@objc private func onBackgroundTap(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.environment?.controller()?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func onSendButtonPressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.animateOutToEmpty = true
|
||||
component.sendMessage(.generic, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) }))
|
||||
self.environment?.controller()?.dismiss()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
if case .initial = self.presentationAnimationState {
|
||||
self.presentationAnimationState = .animatedIn
|
||||
self.state?.updated(transition: .spring(duration: 0.42))
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
if case .animatedOut = self.presentationAnimationState {
|
||||
} else {
|
||||
self.presentationAnimationState = .animatedOut(completion: completion)
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: ChatSendMessageContextScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
let previousAnimationState = self.appliedAnimationState
|
||||
self.appliedAnimationState = self.presentationAnimationState
|
||||
|
||||
let messageActionsSpacing: CGFloat = 7.0
|
||||
|
||||
let alphaTransition: Transition
|
||||
if transition.animation.isImmediate {
|
||||
alphaTransition = .immediate
|
||||
} else {
|
||||
alphaTransition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
let _ = alphaTransition
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
|
||||
let themeUpdated = environment.theme !== self.environment?.theme
|
||||
|
||||
if self.component == nil {
|
||||
component.gesture.externalUpdated = { [weak self] view, location in
|
||||
guard let self, let actionsStackNode = self.actionsStackNode else {
|
||||
return
|
||||
}
|
||||
actionsStackNode.highlightGestureMoved(location: actionsStackNode.view.convert(location, from: view))
|
||||
}
|
||||
component.gesture.externalEnded = { [weak self] viewAndLocation in
|
||||
guard let self, let actionsStackNode = self.actionsStackNode else {
|
||||
return
|
||||
}
|
||||
if let (view, location) = viewAndLocation {
|
||||
actionsStackNode.highlightGestureMoved(location: actionsStackNode.view.convert(location, from: view))
|
||||
actionsStackNode.highlightGestureFinished(performAction: true)
|
||||
} else {
|
||||
actionsStackNode.highlightGestureFinished(performAction: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.environment = environment
|
||||
self.state = state
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundView.updateColor(
|
||||
color: environment.theme.contextMenu.dimColor,
|
||||
enableBlur: true,
|
||||
forceKeepBlur: true,
|
||||
transition: .immediate
|
||||
)
|
||||
}
|
||||
|
||||
let sendButton: HighlightTrackingButton
|
||||
if let current = self.sendButton {
|
||||
sendButton = current
|
||||
} else {
|
||||
sendButton = HighlightTrackingButton()
|
||||
sendButton.accessibilityLabel = environment.strings.MediaPicker_Send
|
||||
sendButton.addTarget(self, action: #selector(self.onSendButtonPressed), for: .touchUpInside)
|
||||
if let snapshotView = component.sourceSendButton.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshotView.isUserInteractionEnabled = false
|
||||
sendButton.addSubview(snapshotView)
|
||||
}
|
||||
self.sendButton = sendButton
|
||||
self.addSubview(sendButton)
|
||||
}
|
||||
|
||||
let sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
|
||||
|
||||
let sendButtonScale: CGFloat
|
||||
switch self.presentationAnimationState {
|
||||
case .initial:
|
||||
sendButtonScale = 0.75
|
||||
default:
|
||||
sendButtonScale = 1.0
|
||||
}
|
||||
|
||||
let messageItemView: MessageItemView
|
||||
if let current = self.messageItemView {
|
||||
messageItemView = current
|
||||
} else {
|
||||
messageItemView = MessageItemView(frame: CGRect())
|
||||
self.messageItemView = messageItemView
|
||||
self.addSubview(messageItemView)
|
||||
}
|
||||
|
||||
let textString: NSAttributedString
|
||||
if let attributedText = component.textInputView.attributedText {
|
||||
textString = attributedText
|
||||
} else {
|
||||
textString = NSAttributedString(string: " ", font: Font.regular(17.0), textColor: .black)
|
||||
}
|
||||
|
||||
let localSourceTextInputViewFrame = convertFrame(component.textInputView.bounds, from: component.textInputView, to: self)
|
||||
|
||||
let sourceMessageTextInsets = UIEdgeInsets(top: 7.0, left: 12.0, bottom: 6.0, right: 20.0)
|
||||
let sourceBackgroundSize = CGSize(width: localSourceTextInputViewFrame.width + 32.0, height: localSourceTextInputViewFrame.height + 4.0)
|
||||
let explicitMessageBackgroundSize: CGSize?
|
||||
switch self.presentationAnimationState {
|
||||
case .initial:
|
||||
explicitMessageBackgroundSize = sourceBackgroundSize
|
||||
case .animatedOut:
|
||||
if self.animateOutToEmpty {
|
||||
explicitMessageBackgroundSize = nil
|
||||
} else {
|
||||
explicitMessageBackgroundSize = sourceBackgroundSize
|
||||
}
|
||||
case .animatedIn:
|
||||
explicitMessageBackgroundSize = nil
|
||||
}
|
||||
|
||||
let messageTextInsets = sourceMessageTextInsets
|
||||
|
||||
let messageItemSize = messageItemView.update(
|
||||
context: component.context,
|
||||
presentationData: presentationData,
|
||||
backgroundNode: component.wallpaperBackgroundNode,
|
||||
textString: textString,
|
||||
textInsets: messageTextInsets,
|
||||
explicitBackgroundSize: explicitMessageBackgroundSize,
|
||||
maxTextWidth: localSourceTextInputViewFrame.width - 32.0,
|
||||
effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil,
|
||||
transition: transition
|
||||
)
|
||||
let sourceMessageItemFrame = CGRect(origin: CGPoint(x: localSourceTextInputViewFrame.minX - sourceMessageTextInsets.left, y: localSourceTextInputViewFrame.minY - 2.0), size: messageItemSize)
|
||||
|
||||
let actionsStackNode: ContextControllerActionsStackNode
|
||||
if let current = self.actionsStackNode {
|
||||
actionsStackNode = current
|
||||
} else {
|
||||
actionsStackNode = ContextControllerActionsStackNode(
|
||||
getController: {
|
||||
return nil
|
||||
},
|
||||
requestDismiss: { _ in
|
||||
},
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(transition))
|
||||
}
|
||||
}
|
||||
)
|
||||
actionsStackNode.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
|
||||
var reminders = false
|
||||
var isSecret = false
|
||||
var canSchedule = false
|
||||
if let peerId = component.peerId {
|
||||
reminders = peerId == component.context.account.peerId
|
||||
isSecret = peerId.namespace == Namespaces.Peer.SecretChat
|
||||
canSchedule = !isSecret
|
||||
}
|
||||
if component.isScheduledMessages {
|
||||
canSchedule = false
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
if !reminders {
|
||||
items.append(.action(ContextMenuActionItem(
|
||||
text: environment.strings.Conversation_SendMessage_SendSilently,
|
||||
icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.animateOutToEmpty = true
|
||||
component.sendMessage(.silently, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) }))
|
||||
self.environment?.controller()?.dismiss()
|
||||
}
|
||||
)))
|
||||
|
||||
if component.canSendWhenOnline && canSchedule {
|
||||
items.append(.action(ContextMenuActionItem(
|
||||
text: environment.strings.Conversation_SendMessage_SendWhenOnline,
|
||||
icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.animateOutToEmpty = true
|
||||
component.sendMessage(.whenOnline, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) }))
|
||||
self.environment?.controller()?.dismiss()
|
||||
}
|
||||
)))
|
||||
}
|
||||
}
|
||||
if canSchedule {
|
||||
items.append(.action(ContextMenuActionItem(
|
||||
text: reminders ? environment.strings.Conversation_SendMessage_SetReminder: environment.strings.Conversation_SendMessage_ScheduleMessage,
|
||||
icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.schedule(nil)
|
||||
self.environment?.controller()?.dismiss()
|
||||
}
|
||||
)))
|
||||
}
|
||||
|
||||
actionsStackNode.push(
|
||||
item: ContextControllerActionsListStackItem(
|
||||
id: nil,
|
||||
items: items,
|
||||
reactionItems: nil,
|
||||
tip: nil,
|
||||
tipSignal: .single(nil),
|
||||
dismissed: nil
|
||||
),
|
||||
currentScrollingState: nil,
|
||||
positionLock: nil,
|
||||
animated: false
|
||||
)
|
||||
self.actionsStackNode = actionsStackNode
|
||||
self.addSubview(actionsStackNode.view)
|
||||
}
|
||||
let actionsStackSize = actionsStackNode.update(
|
||||
presentationData: presentationData,
|
||||
constrainedSize: availableSize,
|
||||
presentation: .modal,
|
||||
transition: transition.containedViewLayoutTransition
|
||||
)
|
||||
let sourceActionsStackFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.minX + 1.0 - actionsStackSize.width, y: sourceMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
|
||||
|
||||
if let reactionItems = component.reactionItems, !reactionItems.isEmpty {
|
||||
let reactionContextNode: ReactionContextNode
|
||||
if let current = self.reactionContextNode {
|
||||
reactionContextNode = current
|
||||
} else {
|
||||
//TODO:localize
|
||||
reactionContextNode = ReactionContextNode(
|
||||
context: component.context,
|
||||
animationCache: component.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
|
||||
return EmojiPagerContentComponent.messageEffectsInputData(
|
||||
context: component.context,
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
hasSearch: true,
|
||||
hideBackground: false
|
||||
)
|
||||
},
|
||||
isExpandedUpdated: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(transition))
|
||||
}
|
||||
},
|
||||
requestLayout: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(transition))
|
||||
}
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(transition))
|
||||
}
|
||||
}
|
||||
)
|
||||
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
|
||||
guard let self, let component = self.component, let reactionContextNode = self.reactionContextNode else {
|
||||
return
|
||||
}
|
||||
|
||||
guard case let .custom(sourceEffectId, _) = updateReaction else {
|
||||
return
|
||||
}
|
||||
|
||||
let messageEffect: Signal<AvailableMessageEffects.MessageEffect?, NoError>
|
||||
messageEffect = component.context.engine.stickers.availableMessageEffects()
|
||||
|> take(1)
|
||||
|> map { availableMessageEffects -> AvailableMessageEffects.MessageEffect? in
|
||||
guard let availableMessageEffects else {
|
||||
return nil
|
||||
}
|
||||
for messageEffect in availableMessageEffects.messageEffects {
|
||||
if messageEffect.id == sourceEffectId || messageEffect.effectSticker.fileId.id == sourceEffectId {
|
||||
return messageEffect
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
self.messageEffectDisposable.set((combineLatest(
|
||||
messageEffect,
|
||||
ReactionContextNode.randomGenericReactionEffect(context: component.context)
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect, path in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
guard let messageEffect else {
|
||||
return
|
||||
}
|
||||
let effectId = messageEffect.id
|
||||
|
||||
let reactionItem = ReactionItem(
|
||||
reaction: ReactionItem.Reaction(rawValue: updateReaction.reaction),
|
||||
appearAnimation: messageEffect.effectSticker,
|
||||
stillAnimation: messageEffect.effectSticker,
|
||||
listAnimation: messageEffect.effectSticker,
|
||||
largeListAnimation: messageEffect.effectSticker,
|
||||
applicationAnimation: nil,
|
||||
largeApplicationAnimation: nil,
|
||||
isCustom: true
|
||||
)
|
||||
|
||||
if let selectedMessageEffect = self.selectedMessageEffect {
|
||||
if selectedMessageEffect.id == effectId {
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
return
|
||||
} else {
|
||||
self.selectedMessageEffect = messageEffect
|
||||
reactionContextNode.selectedItems = Set([AnyHashable(updateReaction.reaction)])
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.selectedMessageEffect = messageEffect
|
||||
reactionContextNode.selectedItems = Set([AnyHashable(updateReaction.reaction)])
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
|
||||
guard let targetView = self.messageItemView?.effectIconView 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)
|
||||
|
||||
var customEffectResource: MediaResource?
|
||||
if let effectAnimation = messageEffect.effectAnimation {
|
||||
customEffectResource = effectAnimation.resource
|
||||
} else {
|
||||
let effectSticker = messageEffect.effectSticker
|
||||
if let effectFile = effectSticker.videoThumbnails.first {
|
||||
customEffectResource = effectFile.resource
|
||||
}
|
||||
}
|
||||
|
||||
standaloneReactionAnimation.animateReactionSelection(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
animationCache: component.context.animationCache,
|
||||
reaction: reactionItem,
|
||||
customEffectResource: customEffectResource,
|
||||
avatarPeers: [],
|
||||
playHaptic: true,
|
||||
isLarge: true,
|
||||
playCenterReaction: false,
|
||||
targetView: targetView,
|
||||
addStandaloneReactionAnimation: { _ in
|
||||
},
|
||||
completion: { [weak standaloneReactionAnimation] in
|
||||
standaloneReactionAnimation?.removeFromSupernode()
|
||||
}
|
||||
)
|
||||
}))
|
||||
}
|
||||
reactionContextNode.displayTail = true
|
||||
reactionContextNode.forceTailToRight = false
|
||||
reactionContextNode.forceDark = false
|
||||
self.reactionContextNode = reactionContextNode
|
||||
self.addSubview(reactionContextNode.view)
|
||||
}
|
||||
}
|
||||
|
||||
var readySendButtonFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.minX, y: sourceSendButtonFrame.minY), size: sourceSendButtonFrame.size)
|
||||
var readyMessageItemFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 8.0 - messageItemSize.width, y: readySendButtonFrame.maxY - 6.0 - messageItemSize.height), size: messageItemSize)
|
||||
var readyActionsStackFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - actionsStackSize.width, y: readyMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
|
||||
|
||||
let bottomOverflow = readyActionsStackFrame.maxY - (availableSize.height - environment.safeInsets.bottom)
|
||||
if bottomOverflow > 0.0 {
|
||||
readyMessageItemFrame.origin.y -= bottomOverflow
|
||||
readyActionsStackFrame.origin.y -= bottomOverflow
|
||||
readySendButtonFrame.origin.y -= bottomOverflow
|
||||
}
|
||||
|
||||
let messageItemFrame: CGRect
|
||||
let actionsStackFrame: CGRect
|
||||
let sendButtonFrame: CGRect
|
||||
switch self.presentationAnimationState {
|
||||
case .initial:
|
||||
messageItemFrame = sourceMessageItemFrame
|
||||
actionsStackFrame = sourceActionsStackFrame
|
||||
sendButtonFrame = sourceSendButtonFrame
|
||||
case .animatedOut:
|
||||
if self.animateOutToEmpty {
|
||||
messageItemFrame = readyMessageItemFrame
|
||||
actionsStackFrame = readyActionsStackFrame
|
||||
sendButtonFrame = readySendButtonFrame
|
||||
} else {
|
||||
messageItemFrame = sourceMessageItemFrame
|
||||
actionsStackFrame = sourceActionsStackFrame
|
||||
sendButtonFrame = sourceSendButtonFrame
|
||||
}
|
||||
case .animatedIn:
|
||||
messageItemFrame = readyMessageItemFrame
|
||||
actionsStackFrame = readyActionsStackFrame
|
||||
sendButtonFrame = readySendButtonFrame
|
||||
}
|
||||
|
||||
transition.setFrame(view: messageItemView, frame: messageItemFrame)
|
||||
|
||||
transition.setPosition(view: actionsStackNode.view, position: CGPoint(x: actionsStackFrame.maxX, y: actionsStackFrame.minY))
|
||||
transition.setBounds(view: actionsStackNode.view, bounds: CGRect(origin: CGPoint(), size: actionsStackFrame.size))
|
||||
if !transition.animation.isImmediate && previousAnimationState.key != self.presentationAnimationState.key {
|
||||
switch self.presentationAnimationState {
|
||||
case .initial:
|
||||
break
|
||||
case .animatedIn:
|
||||
transition.setAlpha(view: actionsStackNode.view, alpha: 1.0)
|
||||
Transition.immediate.setScale(view: actionsStackNode.view, scale: 1.0)
|
||||
actionsStackNode.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
|
||||
case .animatedOut:
|
||||
transition.setAlpha(view: actionsStackNode.view, alpha: 0.0)
|
||||
transition.setScale(view: actionsStackNode.view, scale: 0.001)
|
||||
}
|
||||
} else {
|
||||
switch self.presentationAnimationState {
|
||||
case .animatedIn:
|
||||
transition.setAlpha(view: actionsStackNode.view, alpha: 1.0)
|
||||
transition.setScale(view: actionsStackNode.view, scale: 1.0)
|
||||
case .animatedOut, .initial:
|
||||
transition.setAlpha(view: actionsStackNode.view, alpha: 0.0)
|
||||
transition.setScale(view: actionsStackNode.view, scale: 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
let isFirstTime = reactionContextNode.bounds.isEmpty
|
||||
|
||||
let size = availableSize
|
||||
let reactionsAnchorRect = messageItemFrame
|
||||
transition.setFrame(view: reactionContextNode.view, 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.containedViewLayoutTransition)
|
||||
reactionContextNode.updateIsIntersectingContent(isIntersectingContent: false, transition: .immediate)
|
||||
if isFirstTime {
|
||||
reactionContextNode.animateIn(from: reactionsAnchorRect)
|
||||
} else if self.presentationAnimationState.key == .animatedOut && previousAnimationState.key == .animatedIn {
|
||||
reactionContextNode.animateOut(to: nil, animatingOutToReaction: false)
|
||||
}
|
||||
}
|
||||
if case .animatedOut = self.presentationAnimationState {
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
transition.setPosition(view: sendButton, position: sendButtonFrame.center)
|
||||
transition.setBounds(view: sendButton, bounds: CGRect(origin: CGPoint(), size: sendButtonFrame.size))
|
||||
transition.setScale(view: sendButton, scale: sendButtonScale)
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
self.backgroundView.update(size: availableSize, transition: transition.containedViewLayoutTransition)
|
||||
let backgroundAlpha: CGFloat
|
||||
switch self.presentationAnimationState {
|
||||
case .animatedIn:
|
||||
component.textInputView.isHidden = true
|
||||
component.sourceSendButton.isHidden = true
|
||||
|
||||
backgroundAlpha = 1.0
|
||||
case .animatedOut:
|
||||
backgroundAlpha = 0.0
|
||||
|
||||
if self.animateOutToEmpty {
|
||||
component.textInputView.isHidden = false
|
||||
component.sourceSendButton.isHidden = false
|
||||
|
||||
transition.setAlpha(view: sendButton, alpha: 0.0)
|
||||
if let messageItemView = self.messageItemView {
|
||||
transition.setAlpha(view: messageItemView, alpha: 0.0)
|
||||
}
|
||||
}
|
||||
default:
|
||||
backgroundAlpha = 0.0
|
||||
}
|
||||
|
||||
transition.setAlpha(view: self.backgroundView, alpha: backgroundAlpha, completion: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if case let .animatedOut(completion) = self.presentationAnimationState {
|
||||
if let component = self.component, !self.animateOutToEmpty {
|
||||
component.textInputView.isHidden = false
|
||||
component.sourceSendButton.isHidden = false
|
||||
}
|
||||
completion()
|
||||
}
|
||||
})
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public class ChatSendMessageContextScreen: ViewControllerComponentContainer, ChatSendMessageActionSheetController {
|
||||
private let context: AccountContext
|
||||
|
||||
private var processedDidAppear: Bool = false
|
||||
private var processedDidDisappear: Bool = false
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
|
||||
peerId: EnginePeer.Id?,
|
||||
isScheduledMessages: Bool,
|
||||
forwardMessageIds: [EngineMessage.Id]?,
|
||||
hasEntityKeyboard: Bool,
|
||||
gesture: ContextGesture,
|
||||
sourceSendButton: ASDisplayNode,
|
||||
textInputView: UITextView,
|
||||
emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?,
|
||||
wallpaperBackgroundNode: WallpaperBackgroundNode?,
|
||||
attachment: Bool,
|
||||
canSendWhenOnline: Bool,
|
||||
completion: @escaping () -> Void,
|
||||
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void,
|
||||
reactionItems: [ReactionItem]?
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: ChatSendMessageContextScreenComponent(
|
||||
context: context,
|
||||
peerId: peerId,
|
||||
isScheduledMessages: isScheduledMessages,
|
||||
forwardMessageIds: forwardMessageIds,
|
||||
hasEntityKeyboard: hasEntityKeyboard,
|
||||
gesture: gesture,
|
||||
sourceSendButton: sourceSendButton,
|
||||
textInputView: textInputView,
|
||||
emojiViewProvider: emojiViewProvider,
|
||||
wallpaperBackgroundNode: wallpaperBackgroundNode,
|
||||
attachment: attachment,
|
||||
canSendWhenOnline: canSendWhenOnline,
|
||||
completion: completion,
|
||||
sendMessage: sendMessage,
|
||||
schedule: schedule,
|
||||
reactionItems: reactionItems
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .none,
|
||||
presentationMode: .default
|
||||
)
|
||||
|
||||
self.lockOrientation = true
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
|
||||
/*gesture.externalEnded = { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.dismiss()
|
||||
}*/
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.processedDidAppear {
|
||||
self.processedDidAppear = true
|
||||
if let componentView = self.node.hostView.componentView as? ChatSendMessageContextScreenComponent.View {
|
||||
componentView.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func superDismiss() {
|
||||
super.dismiss()
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.processedDidDisappear {
|
||||
self.processedDidDisappear = true
|
||||
|
||||
if let componentView = self.node.hostView.componentView as? ChatSendMessageContextScreenComponent.View {
|
||||
componentView.animateOut(completion: { [weak self] in
|
||||
if let self {
|
||||
self.superDismiss()
|
||||
}
|
||||
completion?()
|
||||
})
|
||||
} else {
|
||||
super.dismiss(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
318
submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift
Normal file
318
submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift
Normal file
@@ -0,0 +1,318 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
import TelegramCore
|
||||
import TextFormat
|
||||
import ReactionSelectionNode
|
||||
import ViewControllerComponent
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import ChatMessageBackground
|
||||
import WallpaperBackgroundNode
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import ReactionButtonListComponent
|
||||
import MultilineTextComponent
|
||||
|
||||
private final class EffectIcon: Component {
|
||||
enum Content: Equatable {
|
||||
case file(TelegramMediaFile)
|
||||
case text(String)
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let content: Content
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
content: Content
|
||||
) {
|
||||
self.context = context
|
||||
self.content = content
|
||||
}
|
||||
|
||||
static func ==(lhs: EffectIcon, rhs: EffectIcon) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var fileView: ReactionIconView?
|
||||
private var textView: ComponentView<Empty>?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: EffectIcon, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
if case let .file(file) = component.content {
|
||||
let fileView: ReactionIconView
|
||||
if let current = self.fileView {
|
||||
fileView = current
|
||||
} else {
|
||||
fileView = ReactionIconView()
|
||||
self.fileView = fileView
|
||||
self.addSubview(fileView)
|
||||
}
|
||||
fileView.update(
|
||||
size: availableSize,
|
||||
context: component.context,
|
||||
file: file,
|
||||
fileId: file.fileId.id,
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
tintColor: nil,
|
||||
placeholderColor: UIColor(white: 0.0, alpha: 0.1),
|
||||
animateIdle: false,
|
||||
reaction: .custom(file.fileId.id),
|
||||
transition: .immediate
|
||||
)
|
||||
fileView.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
} else {
|
||||
if let fileView = self.fileView {
|
||||
self.fileView = nil
|
||||
fileView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
if case let .text(text) = component.content {
|
||||
let textView: ComponentView<Empty>
|
||||
if let current = self.textView {
|
||||
textView = current
|
||||
} else {
|
||||
textView = ComponentView()
|
||||
self.textView = textView
|
||||
}
|
||||
let textSize = textView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: text, font: Font.regular(10.0), textColor: .black))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - textSize.height) * 0.5)), size: textSize)
|
||||
if let textComponentView = textView.view {
|
||||
if textComponentView.superview == nil {
|
||||
self.addSubview(textComponentView)
|
||||
}
|
||||
textComponentView.frame = textFrame
|
||||
}
|
||||
} else {
|
||||
if let textView = self.textView {
|
||||
self.textView = nil
|
||||
textView.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class MessageItemView: UIView {
|
||||
private let backgroundWallpaperNode: ChatMessageBubbleBackdrop
|
||||
private let backgroundNode: ChatMessageBackground
|
||||
|
||||
private let text = ComponentView<Empty>()
|
||||
|
||||
private var effectIcon: ComponentView<Empty>?
|
||||
var effectIconView: UIView? {
|
||||
return self.effectIcon?.view
|
||||
}
|
||||
|
||||
private var chatTheme: ChatPresentationThemeData?
|
||||
private var currentSize: CGSize?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundWallpaperNode = ChatMessageBubbleBackdrop()
|
||||
self.backgroundNode = ChatMessageBackground()
|
||||
self.backgroundNode.backdropNode = self.backgroundWallpaperNode
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundWallpaperNode.view)
|
||||
self.addSubview(self.backgroundNode.view)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(
|
||||
context: AccountContext,
|
||||
presentationData: PresentationData,
|
||||
backgroundNode: WallpaperBackgroundNode?,
|
||||
textString: NSAttributedString,
|
||||
textInsets: UIEdgeInsets,
|
||||
explicitBackgroundSize: CGSize?,
|
||||
maxTextWidth: CGFloat,
|
||||
effect: AvailableMessageEffects.MessageEffect?,
|
||||
transition: Transition
|
||||
) -> CGSize {
|
||||
var effectIconSize: CGSize?
|
||||
if let effect {
|
||||
let effectIcon: ComponentView<Empty>
|
||||
if let current = self.effectIcon {
|
||||
effectIcon = current
|
||||
} else {
|
||||
effectIcon = ComponentView()
|
||||
self.effectIcon = effectIcon
|
||||
}
|
||||
let effectIconContent: EffectIcon.Content
|
||||
if let staticIcon = effect.staticIcon {
|
||||
effectIconContent = .file(staticIcon)
|
||||
} else {
|
||||
effectIconContent = .text(effect.emoticon)
|
||||
}
|
||||
effectIconSize = effectIcon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(EffectIcon(
|
||||
context: context,
|
||||
content: effectIconContent
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 8.0, height: 8.0)
|
||||
)
|
||||
}
|
||||
|
||||
var textCutout: TextNodeCutout?
|
||||
if let effectIconSize {
|
||||
textCutout = TextNodeCutout(bottomRight: CGSize(width: effectIconSize.width + 4.0, height: effectIconSize.height))
|
||||
}
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextWithEntitiesComponent(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
placeholderColor: presentationData.theme.chat.message.stickerPlaceholderColor.withWallpaper,
|
||||
text: .plain(textString),
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.12,
|
||||
cutout: textCutout,
|
||||
insets: UIEdgeInsets()
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: maxTextWidth, height: 20000.0)
|
||||
)
|
||||
|
||||
let size = CGSize(width: textSize.width + textInsets.left + textInsets.right, height: textSize.height + textInsets.top + textInsets.bottom)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: textSize)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
textView.frame = textFrame
|
||||
}
|
||||
|
||||
let chatTheme: ChatPresentationThemeData
|
||||
if let current = self.chatTheme, current.theme === presentationData.theme {
|
||||
chatTheme = current
|
||||
} else {
|
||||
chatTheme = ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper)
|
||||
self.chatTheme = chatTheme
|
||||
}
|
||||
|
||||
let themeGraphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper, bubbleCorners: presentationData.chatBubbleCorners)
|
||||
self.backgroundWallpaperNode.setType(
|
||||
type: .outgoing(.None),
|
||||
theme: chatTheme,
|
||||
essentialGraphics: themeGraphics,
|
||||
maskMode: true,
|
||||
backgroundNode: backgroundNode
|
||||
)
|
||||
self.backgroundNode.setType(
|
||||
type: .outgoing(.None),
|
||||
highlighted: false,
|
||||
graphics: themeGraphics,
|
||||
maskMode: true,
|
||||
hasWallpaper: true,
|
||||
transition: transition.containedViewLayoutTransition,
|
||||
backgroundNode: backgroundNode
|
||||
)
|
||||
|
||||
let backgroundSize = explicitBackgroundSize ?? size
|
||||
|
||||
let previousSize = self.currentSize
|
||||
self.currentSize = backgroundSize
|
||||
|
||||
if let effectIcon = self.effectIcon, let effectIconSize {
|
||||
if let effectIconView = effectIcon.view {
|
||||
var animateIn = false
|
||||
if effectIconView.superview == nil {
|
||||
animateIn = true
|
||||
self.addSubview(effectIconView)
|
||||
}
|
||||
let effectIconFrame = CGRect(origin: CGPoint(x: backgroundSize.width - textInsets.right + 2.0 - effectIconSize.width, y: backgroundSize.height - textInsets.bottom - 2.0 - effectIconSize.height), size: effectIconSize)
|
||||
if animateIn {
|
||||
if let previousSize {
|
||||
let previousEffectIconFrame = CGRect(origin: CGPoint(x: previousSize.width - textInsets.right + 2.0 - effectIconSize.width, y: previousSize.height - textInsets.bottom - 2.0 - effectIconSize.height), size: effectIconSize)
|
||||
effectIconView.frame = previousEffectIconFrame
|
||||
} else {
|
||||
effectIconView.frame = effectIconFrame
|
||||
}
|
||||
transition.animateAlpha(view: effectIconView, from: 0.0, to: 1.0)
|
||||
if !transition.animation.isImmediate {
|
||||
effectIconView.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||
}
|
||||
}
|
||||
|
||||
transition.setFrame(view: effectIconView, frame: effectIconFrame)
|
||||
}
|
||||
} else {
|
||||
if let effectIcon = self.effectIcon {
|
||||
self.effectIcon = nil
|
||||
|
||||
if let effectIconView = effectIcon.view {
|
||||
let effectIconSize = effectIconView.bounds.size
|
||||
let effectIconFrame = CGRect(origin: CGPoint(x: backgroundSize.width - textInsets.right - effectIconSize.width, y: backgroundSize.height - textInsets.bottom - effectIconSize.height), size: effectIconSize)
|
||||
transition.setFrame(view: effectIconView, frame: effectIconFrame)
|
||||
transition.setScale(view: effectIconView, scale: 0.001)
|
||||
transition.setAlpha(view: effectIconView, alpha: 0.0, completion: { [weak effectIconView] _ in
|
||||
effectIconView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let backgroundAlpha: CGFloat
|
||||
if explicitBackgroundSize != nil {
|
||||
backgroundAlpha = 0.0
|
||||
} else {
|
||||
backgroundAlpha = 1.0
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundWallpaperNode.view, frame: CGRect(origin: CGPoint(), size: backgroundSize))
|
||||
transition.setAlpha(view: self.backgroundWallpaperNode.view, alpha: backgroundAlpha)
|
||||
self.backgroundWallpaperNode.updateFrame(CGRect(origin: CGPoint(), size: backgroundSize), transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: backgroundSize))
|
||||
transition.setAlpha(view: self.backgroundNode.view, alpha: backgroundAlpha)
|
||||
self.backgroundNode.updateLayout(size: backgroundSize, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
return backgroundSize
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user