[WIP] Send message effects

This commit is contained in:
Isaac
2024-05-03 22:56:50 +04:00
parent 18a6a3c2a9
commit 16faaa4575
103 changed files with 3113 additions and 841 deletions

View File

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

View File

@@ -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
)
}

View File

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

View File

@@ -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)
}
}
}
}

View 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
}
}