Telegram-iOS/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift
isaac 1d8931b334 Render markdown rich-text preview in send-message context screen
When the user long-presses Send in the main chat and the composed text passes the
same rich-markdown gate used at send time (richMarkdownAttributeIfNeeded), the
send-options preview bubble renders the message as rich InstantPage content,
crossfading from the raw text as the menu animates in. Plain text and custom chat
contents / edit previews are unchanged.

ChatSendMessageActionUI cannot depend on InstantPageUI (it closes a dependency
cycle via LocationUI -> AttachmentUI -> ... -> ChatTextInputActionButtonsNode), so
the rich view is built in TelegramUI (ChatSendMessageRichTextPreview, wrapping
InstantPageV2View + the outgoing message theme) and injected into the screen via a
new ChatSendMessageContextScreenRichTextPreview protocol, mirroring the existing
media-preview pattern. The main-chat send call site classifies the compose text
and injects the preview; MessageItemView lays it out within the send-button-bounded
bubble envelope and crossfades between the raw text and the rich layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:46:16 +02:00

1616 lines
82 KiB
Swift

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 WallpaperBackgroundNode
import ReactionSelectionNode
import EntityKeyboard
import LottieMetal
import TelegramAnimatedStickerNode
import AnimatedStickerNode
import ChatInputTextNode
import UndoUI
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
}
public enum ChatSendMessageContextScreenMediaPreviewLayoutType {
case message
case media
case videoMessage
}
public protocol ChatSendMessageContextScreenMediaPreview: AnyObject {
var isReady: Signal<Bool, NoError> { get }
var view: UIView { get }
var globalClippingRect: CGRect? { get }
var layoutType: ChatSendMessageContextScreenMediaPreviewLayoutType { get }
func animateIn(transition: ComponentTransition)
func animateOut(transition: ComponentTransition)
func animateOutOnSend(transition: ComponentTransition)
func update(containerSize: CGSize, transition: ComponentTransition) -> CGSize
}
public protocol ChatSendMessageContextScreenRichTextPreview: AnyObject {
var view: UIView { get }
// Lays out the rich content for the given bubble width and theme, returning its content
// size. Called every layout pass; the implementation memoizes internally.
func update(boundingWidth: CGFloat, presentationData: PresentationData, transition: ComponentTransition) -> CGSize
}
final class ChatSendMessageContextScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let initialData: ChatSendMessageContextScreen.InitialData
let context: AccountContext
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
let peerId: EnginePeer.Id?
let params: SendMessageActionSheetControllerParams
let hasEntityKeyboard: Bool
let gesture: ContextGesture?
let sourceSendButton: UIView
let textInputView: UITextView?
let emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
let wallpaperBackgroundNode: WallpaperBackgroundNode?
let completion: () -> Void
let sendMessage: (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void
let schedule: (ChatSendMessageActionSheetController.SendParameters?) -> Void
let editPrice: (Int64) -> Void
let openPremiumPaywall: (ViewController) -> Void
let reactionItems: [ReactionItem]?
let availableMessageEffects: AvailableMessageEffects?
let isPremium: Bool
let richTextPreview: ChatSendMessageContextScreenRichTextPreview?
init(
initialData: ChatSendMessageContextScreen.InitialData,
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
peerId: EnginePeer.Id?,
params: SendMessageActionSheetControllerParams,
hasEntityKeyboard: Bool,
gesture: ContextGesture?,
sourceSendButton: UIView,
textInputView: UITextView?,
emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?,
wallpaperBackgroundNode: WallpaperBackgroundNode?,
completion: @escaping () -> Void,
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void,
schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void,
editPrice: @escaping (Int64) -> Void,
openPremiumPaywall: @escaping (ViewController) -> Void,
reactionItems: [ReactionItem]?,
availableMessageEffects: AvailableMessageEffects?,
isPremium: Bool,
richTextPreview: ChatSendMessageContextScreenRichTextPreview? = nil
) {
self.initialData = initialData
self.context = context
self.updatedPresentationData = updatedPresentationData
self.peerId = peerId
self.params = params
self.hasEntityKeyboard = hasEntityKeyboard
self.gesture = gesture
self.sourceSendButton = sourceSendButton
self.textInputView = textInputView
self.emojiViewProvider = emojiViewProvider
self.wallpaperBackgroundNode = wallpaperBackgroundNode
self.completion = completion
self.sendMessage = sendMessage
self.schedule = schedule
self.editPrice = editPrice
self.openPremiumPaywall = openPremiumPaywall
self.reactionItems = reactionItems
self.availableMessageEffects = availableMessageEffects
self.isPremium = isPremium
self.richTextPreview = richTextPreview
}
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: SendButton?
private var messageItemView: MessageItemView?
private var internalWallpaperBackgroundNode: WallpaperBackgroundNode?
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 var mediaCaptionIsAbove: Bool = false
private let messageEffectDisposable = MetaDisposable()
private var selectedMessageEffect: AvailableMessageEffects.MessageEffect?
private var standaloneReactionAnimation: AnimatedStickerNode?
private var isLoadingEffectAnimation: Bool = false
private var isLoadingEffectAnimationTimerDisposable: Disposable?
private var loadEffectAnimationDisposable: Disposable?
private var animateInTimestamp: Double?
private var performedActionsOnAnimateOut: Bool = false
private var presentationAnimationState: PresentationAnimationState = .initial
private var appliedAnimationState: PresentationAnimationState = .initial
private var animateOutToEmpty: Bool = false
private var initializationDisplayLink: SharedDisplayLinkDriver.Link?
private var updateSourcePositionsDisplayLink: SharedDisplayLinkDriver.Link?
private var stableSourceSendButtonFrame: CGRect?
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()
self.loadEffectAnimationDisposable?.dispose()
self.isLoadingEffectAnimationTimerDisposable?.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
self.environment?.controller()?.dismiss()
let sendParameters = ChatSendMessageActionSheetController.SendParameters(
effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }),
textIsAboveMedia: self.mediaCaptionIsAbove
)
component.sendMessage(.generic, sendParameters)
}
func animateIn() {
if case .initial = self.presentationAnimationState {
HapticFeedback().impact()
self.animateInTimestamp = CFAbsoluteTimeGetCurrent()
self.presentationAnimationState = .animatedIn
self.state?.updated(transition: .spring(duration: 0.42))
}
}
func animateOut(completion: @escaping () -> Void) {
if let controller = self.environment?.controller() {
controller.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
}
return true
}
}
if case .animatedOut = self.presentationAnimationState {
} else {
self.presentationAnimationState = .animatedOut(completion: completion)
self.state?.updated(transition: .spring(duration: 0.4))
}
}
private func requestUpdateOverlayWantsToBeBelowKeyboard(transition: ContainedViewLayoutTransition) {
guard let controller = self.environment?.controller() as? ChatSendMessageContextScreen else {
return
}
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
}
func wantsToBeBelowKeyboard() -> Bool {
if let reactionContextNode = self.reactionContextNode {
return reactionContextNode.wantsDisplayBelowKeyboard()
}
return false
}
func update(component: ChatSendMessageContextScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[EnvironmentType.self].value
if let previousEnvironment = self.environment, previousEnvironment.inputHeight != 0.0, environment.inputHeight == 0.0 {
DispatchQueue.main.async { [weak self] in
guard let self, let component = self.component else {
return
}
let stableSourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton, to: self)
if self.stableSourceSendButtonFrame != stableSourceSendButtonFrame {
self.stableSourceSendButtonFrame = stableSourceSendButtonFrame
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.35))
}
}
}
}
var transition = transition
var transitionIsImmediate = transition.animation.isImmediate
if case let .curve(duration, _) = transition.animation, duration == 0.0 {
transitionIsImmediate = true
}
if transitionIsImmediate, let previousEnvironment = self.environment, previousEnvironment.inputHeight != 0.0, environment.inputHeight != 0.0, previousEnvironment.inputHeight != environment.inputHeight {
transition = .spring(duration: 0.4)
}
let previousAnimationState = self.appliedAnimationState
self.appliedAnimationState = self.presentationAnimationState
let messageActionsSpacing: CGFloat = 7.0
let alphaTransition: ComponentTransition
if transition.animation.isImmediate {
alphaTransition = .immediate
} else {
alphaTransition = .easeInOut(duration: 0.25)
}
let _ = alphaTransition
let themeUpdated = environment.theme !== self.environment?.theme
if self.component == nil {
switch component.params {
case let .sendMessage(sendMessage):
self.mediaCaptionIsAbove = sendMessage.mediaCaptionIsAbove?.0 ?? false
self.selectedMessageEffect = component.initialData.messageEffect
case let .editMessage(editMessage):
self.mediaCaptionIsAbove = editMessage.mediaCaptionIsAbove?.0 ?? false
}
component.gesture?.externalUpdated = { [weak self] view, location in
guard let self, let actionsStackNode = self.actionsStackNode else {
return
}
guard let animateInTimestamp = self.animateInTimestamp, animateInTimestamp < CFAbsoluteTimeGetCurrent() - 0.35 else {
return
}
let localPoint: CGPoint
if let metrics = self.environment?.metrics, metrics.isTablet, availableSize.width > availableSize.height, let view {
localPoint = view.convert(location, to: nil)
} else {
localPoint = self.convert(location, from: view)
}
actionsStackNode.highlightGestureMoved(location: self.convert(localPoint, to: actionsStackNode.view))
}
component.gesture?.externalEnded = { [weak self] viewAndLocation in
guard let self, let actionsStackNode = self.actionsStackNode else {
return
}
guard let animateInTimestamp = self.animateInTimestamp, animateInTimestamp < CFAbsoluteTimeGetCurrent() - 0.35 else {
actionsStackNode.highlightGestureFinished(performAction: false)
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.updatedPresentationData?.initial ?? component.context.sharedContext.currentPresentationData.with({ $0 })
if themeUpdated {
self.backgroundView.updateColor(
color: environment.theme.contextMenu.dimColor,
enableBlur: true,
forceKeepBlur: true,
transition: .immediate
)
}
var mediaPreview: ChatSendMessageContextScreenMediaPreview?
switch component.params {
case let .sendMessage(sendMessage):
mediaPreview = sendMessage.mediaPreview
case let .editMessage(editMessage):
mediaPreview = editMessage.mediaPreview
}
var isMessageVisible: Bool = mediaPreview != nil
let textString: NSAttributedString
if let attributedText = component.textInputView?.attributedText {
textString = attributedText
if textString.length != 0 {
isMessageVisible = true
}
} else {
textString = NSAttributedString(string: " ", font: Font.regular(17.0), textColor: .black)
}
let sendButton: SendButton
if let current = self.sendButton {
sendButton = current
} else {
let sendButtonKind: SendButton.Kind
switch component.params {
case .sendMessage:
sendButtonKind = .send
case .editMessage:
sendButtonKind = .edit
}
sendButton = SendButton(kind: sendButtonKind)
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: CGRect
switch self.presentationAnimationState {
case .animatedOut:
sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton, to: self)
self.stableSourceSendButtonFrame = sourceSendButtonFrame
default:
if let stableSourceSendButtonFrame = self.stableSourceSendButtonFrame {
sourceSendButtonFrame = stableSourceSendButtonFrame
} else {
sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton, to: self)
self.stableSourceSendButtonFrame = sourceSendButtonFrame
}
}
let sendButtonScale: CGFloat
if component.sourceSendButton is ContextExtractedContentContainingView {
sendButtonScale = 1.0
} else {
switch self.presentationAnimationState {
case .initial:
sendButtonScale = 0.75
default:
sendButtonScale = 1.0
}
}
var reminders = false
var isSecret = false
var canSchedule = false
var canMakePaidContent = false
var currentPrice: Int64?
switch component.params {
case let .sendMessage(sendMessage):
if let peerId = component.peerId {
reminders = peerId == component.context.account.peerId
isSecret = peerId.namespace == Namespaces.Peer.SecretChat
canSchedule = !isSecret
}
if sendMessage.isScheduledMessages {
canSchedule = false
}
if sendMessage.hasTimers {
canSchedule = false
}
if let _ = sendMessage.sendPaidMessageStars {
canSchedule = false
}
if sendMessage.isMonoforum {
canSchedule = false
}
canMakePaidContent = sendMessage.canMakePaidContent
currentPrice = sendMessage.currentPrice
case .editMessage:
break
}
var items: [ContextMenuItem] = []
let canAdjustMediaCaptionPosition: Bool
switch component.params {
case let .sendMessage(sendMessage):
if case .media = mediaPreview?.layoutType {
canAdjustMediaCaptionPosition = sendMessage.mediaCaptionIsAbove != nil
} else {
canAdjustMediaCaptionPosition = false
}
case .editMessage:
if case .media = mediaPreview?.layoutType {
canAdjustMediaCaptionPosition = textString.length != 0
} else {
canAdjustMediaCaptionPosition = false
}
}
if canAdjustMediaCaptionPosition, textString.length != 0 {
let mediaCaptionIsAbove = self.mediaCaptionIsAbove
items.append(.action(ContextMenuActionItem(
id: AnyHashable("captionPosition"),
text: mediaCaptionIsAbove ? presentationData.strings.Chat_SendMessageMenu_MoveCaptionDown : presentationData.strings.Chat_SendMessageMenu_MoveCaptionUp,
icon: { _ in
return nil
}, iconAnimation: ContextMenuActionItem.IconAnimation(
name: !mediaCaptionIsAbove ? "message_preview_sort_above" : "message_preview_sort_below"
), action: { [weak self] _, _ in
guard let self, let component = self.component else {
return
}
self.mediaCaptionIsAbove = !self.mediaCaptionIsAbove
switch component.params {
case let .sendMessage(sendMessage):
sendMessage.mediaCaptionIsAbove?.1(self.mediaCaptionIsAbove)
case let .editMessage(editMessage):
editMessage.mediaCaptionIsAbove?.1(self.mediaCaptionIsAbove)
}
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.35))
}
}
)))
items.append(.separator)
}
switch component.params {
case let.sendMessage(sendMessage):
if !reminders {
items.append(.action(ContextMenuActionItem(
id: AnyHashable("silent"),
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
let sendParameters = ChatSendMessageActionSheetController.SendParameters(
effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }),
textIsAboveMedia: self.mediaCaptionIsAbove
)
component.sendMessage(.silently, sendParameters)
self.environment?.controller()?.dismiss()
}
)))
if sendMessage.canSendWhenOnline && canSchedule {
items.append(.action(ContextMenuActionItem(
id: AnyHashable("whenOnline"),
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
let sendParameters = ChatSendMessageActionSheetController.SendParameters(
effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }),
textIsAboveMedia: self.mediaCaptionIsAbove
)
component.sendMessage(.whenOnline, sendParameters)
self.environment?.controller()?.dismiss()
}
)))
}
}
if canSchedule {
items.append(.action(ContextMenuActionItem(
id: AnyHashable("schedule"),
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
}
self.animateOutToEmpty = true
let sendParameters = ChatSendMessageActionSheetController.SendParameters(
effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }),
textIsAboveMedia: self.mediaCaptionIsAbove
)
component.schedule(sendParameters)
self.environment?.controller()?.dismiss()
}
)))
}
if canMakePaidContent {
let title: String
let titleLayout: ContextMenuActionItemTextLayout
if let currentPrice {
title = environment.strings.Attachment_Paid_EditPrice
titleLayout = .secondLineWithValue(environment.strings.Attachment_Paid_EditPrice_Stars(Int32(clamping: currentPrice)))
} else {
title = environment.strings.Attachment_Paid_Create
titleLayout = .twoLinesMax
}
items.append(.action(ContextMenuActionItem(
id: AnyHashable("paid"),
text: title,
textLayout: titleLayout,
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, _ in
guard let self, let component = self.component, case let .sendMessage(params) = component.params else {
return
}
let editPrice = component.editPrice
let controller = component.context.sharedContext.makeStarsAmountScreen(context: component.context, initialValue: params.currentPrice, completion: { amount in
editPrice(amount)
})
self.environment?.controller()?.dismiss()
Queue.mainQueue().after(0.45) {
component.openPremiumPaywall(controller)
}
}
)))
}
case .editMessage:
items.append(.action(ContextMenuActionItem(
id: AnyHashable("silent"),
text: environment.strings.Chat_SendMessageMenu_EditMessage,
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, _ in
guard let self, let component = self.component else {
return
}
self.animateOutToEmpty = true
let sendParameters = ChatSendMessageActionSheetController.SendParameters(
effect: nil,
textIsAboveMedia: self.mediaCaptionIsAbove
)
component.sendMessage(.generic, sendParameters)
self.environment?.controller()?.dismiss()
}
)))
}
if case .separator = items.last {
items.removeLast()
}
let actionsStackNode: ContextControllerActionsStackNode
if let current = self.actionsStackNode {
actionsStackNode = current
actionsStackNode.replace(item: makeContextControllerActionsListStackItem(
id: AnyHashable("items"),
items: items,
reactionItems: nil,
previewReaction: nil,
tip: nil,
tipSignal: .single(nil),
dismissed: nil
), animated: !transition.animation.isImmediate)
} else {
actionsStackNode = makeContextControllerActionsStackNode(
context: component.context,
getController: {
return nil
},
requestDismiss: { _ in
},
requestUpdate: { [weak self] transition in
guard let self else {
return
}
if !self.isUpdating {
self.state?.updated(transition: ComponentTransition(transition))
}
}
)
if isMessageVisible {
actionsStackNode.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0)
}
actionsStackNode.push(
item: makeContextControllerActionsListStackItem(
id: AnyHashable("items"),
items: items,
reactionItems: nil,
previewReaction: 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 messageItemView: MessageItemView
if let current = self.messageItemView {
messageItemView = current
} else {
messageItemView = MessageItemView(frame: CGRect())
self.messageItemView = messageItemView
self.addSubview(messageItemView)
}
let wallpaperBackgroundNode: WallpaperBackgroundNode
if let externalWallpaperBackgroundNode = component.wallpaperBackgroundNode {
wallpaperBackgroundNode = externalWallpaperBackgroundNode
} else if let current = self.internalWallpaperBackgroundNode {
wallpaperBackgroundNode = current
wallpaperBackgroundNode.frame = CGRect(origin: CGPoint(), size: availableSize)
wallpaperBackgroundNode.updateLayout(size: availableSize, displayMode: .aspectFill, transition: .immediate)
} else {
wallpaperBackgroundNode = createWallpaperBackgroundNode(context: component.context, forChatDisplay: true, useSharedAnimationPhase: false)
wallpaperBackgroundNode.frame = CGRect(origin: CGPoint(), size: availableSize)
wallpaperBackgroundNode.updateLayout(size: availableSize, displayMode: .aspectFill, transition: .immediate)
wallpaperBackgroundNode.updateBubbleTheme(bubbleTheme: presentationData.theme, bubbleCorners: presentationData.chatBubbleCorners)
wallpaperBackgroundNode.update(wallpaper: presentationData.chatWallpaper, animated: false)
self.internalWallpaperBackgroundNode = wallpaperBackgroundNode
self.insertSubview(wallpaperBackgroundNode.view, at: 0)
wallpaperBackgroundNode.alpha = 0.0
}
let localSourceTextInputViewFrame: CGRect
if let textInputView = component.textInputView {
localSourceTextInputViewFrame = convertFrame(textInputView.bounds, from: textInputView, to: self)
} else {
localSourceTextInputViewFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton, 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 messageItemViewContainerSize: CGSize
if let mediaPreview {
switch mediaPreview.layoutType {
case .message, .media:
messageItemViewContainerSize = CGSize(width: availableSize.width - 16.0 - 40.0, height: availableSize.height)
case .videoMessage:
messageItemViewContainerSize = CGSize(width: availableSize.width, height: availableSize.height)
}
} else {
messageItemViewContainerSize = CGSize(width: availableSize.width - 16.0 - 40.0, height: availableSize.height)
}
var isEditMessage = false
if case .editMessage = component.params {
isEditMessage = true
}
// The bubble's right edge is pinned to the send button (see readyMessageItemFrame).
// Constrain the rich layout to the span between that edge and a fixed left margin
// (independent of where the source text input sits) so it isn't laid out at the
// full container width.
let richSendButtonWidth: CGFloat
if component.sourceSendButton is ContextExtractedContentContainingView {
richSendButtonWidth = sourceSendButtonFrame.width
} else {
richSendButtonWidth = min(sourceSendButtonFrame.width, 40.0)
}
let richBubbleRightEdge = sourceSendButtonFrame.maxX - richSendButtonWidth
let richBubbleLeftMargin: CGFloat = 16.0
let maxRichBubbleWidth = max(1.0, min(messageItemViewContainerSize.width, richBubbleRightEdge - richBubbleLeftMargin))
let messageItemSize = messageItemView.update(
context: component.context,
presentationData: presentationData,
backgroundNode: wallpaperBackgroundNode,
textString: textString,
richTextPreview: component.richTextPreview,
maxRichBubbleWidth: maxRichBubbleWidth,
sourceTextInputView: component.textInputView as? ChatInputTextView,
emojiViewProvider: component.emojiViewProvider,
sourceMediaPreview: mediaPreview,
mediaCaptionIsAbove: self.mediaCaptionIsAbove,
textInsets: messageTextInsets,
explicitBackgroundSize: explicitMessageBackgroundSize,
maxTextWidth: component.richTextPreview != nil ? maxRichBubbleWidth : localSourceTextInputViewFrame.width,
maxTextHeight: 20000.0,
containerSize: messageItemViewContainerSize,
effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil,
isEditMessage: isEditMessage,
transition: transition
)
let sourceMessageItemFrame = CGRect(origin: CGPoint(x: localSourceTextInputViewFrame.minX - sourceMessageTextInsets.left, y: localSourceTextInputViewFrame.minY - 2.0), size: messageItemSize)
if let reactionItems = component.reactionItems, !reactionItems.isEmpty {
let reactionContextNode: ReactionContextNode
if let current = self.reactionContextNode {
reactionContextNode = current
} else {
reactionContextNode = ReactionContextNode(
context: component.context,
animationCache: component.context.animationCache,
presentationData: presentationData,
items: reactionItems.map { item in
var icon: EmojiPagerContentComponent.Item.Icon = .none
if !component.isPremium, case let .custom(sourceEffectId) = item.reaction.rawValue, let availableMessageEffects = component.availableMessageEffects {
for messageEffect in availableMessageEffects.messageEffects {
if messageEffect.id == sourceEffectId || messageEffect.effectSticker.fileId.id == sourceEffectId {
if messageEffect.isPremium {
icon = .locked
}
break
}
}
}
return ReactionContextItem.reaction(item: item, icon: icon)
},
selectedItems: Set(),
title: presentationData.strings.Chat_MessageEffectMenu_TitleAddEffect,
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: ComponentTransition(transition))
}
},
requestLayout: { [weak self] transition in
guard let self else {
return
}
if !self.isUpdating {
self.state?.updated(transition: ComponentTransition(transition))
}
},
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
guard let self else {
return
}
self.requestUpdateOverlayWantsToBeBelowKeyboard(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((messageEffect
|> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect in
guard let self, let component = self.component else {
return
}
guard let messageEffect else {
return
}
let effectId = messageEffect.id
if let selectedMessageEffect = self.selectedMessageEffect {
if selectedMessageEffect.id == effectId {
self.selectedMessageEffect = nil
reactionContextNode.selectedItems = Set([])
self.loadEffectAnimationDisposable?.dispose()
self.isLoadingEffectAnimationTimerDisposable?.dispose()
self.isLoadingEffectAnimationTimerDisposable = nil
self.isLoadingEffectAnimation = false
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))
}
if case let .sendMessage(sendMessage) = component.params {
let mappedEffect = self.selectedMessageEffect.flatMap {
return ChatSendMessageActionSheetControllerSendParameters.Effect(id: $0.id)
}
sendMessage.messageEffect?.1(mappedEffect)
}
return
} else {
self.selectedMessageEffect = messageEffect
reactionContextNode.selectedItems = Set([AnyHashable(updateReaction.reaction)])
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
if case let .sendMessage(sendMessage) = component.params {
let mappedEffect = self.selectedMessageEffect.flatMap {
return ChatSendMessageActionSheetControllerSendParameters.Effect(id: $0.id)
}
sendMessage.messageEffect?.1(mappedEffect)
}
HapticFeedback().tap()
}
} else {
self.selectedMessageEffect = messageEffect
reactionContextNode.selectedItems = Set([AnyHashable(updateReaction.reaction)])
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
if case let .sendMessage(sendMessage) = component.params {
let mappedEffect = self.selectedMessageEffect.flatMap {
return ChatSendMessageActionSheetControllerSendParameters.Effect(id: $0.id)
}
sendMessage.messageEffect?.1(mappedEffect)
}
HapticFeedback().tap()
}
self.loadEffectAnimationDisposable?.dispose()
self.isLoadingEffectAnimationTimerDisposable?.dispose()
self.isLoadingEffectAnimationTimerDisposable = (Signal<Never, NoError>.complete() |> delay(0.2, queue: .mainQueue()) |> deliverOnMainQueue).startStrict(completed: { [weak self] in
guard let self else {
return
}
self.isLoadingEffectAnimation = true
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
})
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()
})
}
var customEffectResource: (FileMediaReference, TelegramMediaResource)?
if let effectAnimation = messageEffect.effectAnimation?._parse() {
customEffectResource = (FileMediaReference.standalone(media: effectAnimation), effectAnimation.resource)
} else {
let effectSticker = messageEffect.effectSticker._parse()
if let effectFile = effectSticker.videoThumbnails.first {
customEffectResource = (FileMediaReference.standalone(media: effectSticker), effectFile.resource)
}
}
guard let (customEffectResourceFileReference, customEffectResource) = customEffectResource else {
return
}
let context = component.context
var loadEffectAnimationSignal: Signal<Never, NoError>
loadEffectAnimationSignal = Signal { subscriber in
let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: customEffectResourceFileReference, resource: customEffectResource).start()
let dataDisposabke = (context.engine.resources.status(resource: EngineMediaResource(customEffectResource))
|> filter { status in
if status == .Local {
return true
} else {
return false
}
}
|> take(1)).start(next: { _ in
subscriber.putCompletion()
})
return ActionDisposable {
fetchDisposable.dispose()
dataDisposabke.dispose()
}
}
/*#if DEBUG
loadEffectAnimationSignal = loadEffectAnimationSignal |> delay(1.0, queue: .mainQueue())
#endif*/
self.loadEffectAnimationDisposable = (loadEffectAnimationSignal
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self, let component = self.component else {
return
}
self.isLoadingEffectAnimationTimerDisposable?.dispose()
self.isLoadingEffectAnimationTimerDisposable = nil
self.isLoadingEffectAnimation = false
guard let targetView = self.messageItemView?.effectIconView else {
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
return
}
let standaloneReactionAnimation: AnimatedStickerNode
var effectiveScale: CGFloat = 1.0
#if targetEnvironment(simulator)
standaloneReactionAnimation = DirectAnimatedStickerNode()
effectiveScale = 1.4
#else
standaloneReactionAnimation = DirectAnimatedStickerNode()
effectiveScale = 1.4
/*if "".isEmpty {
standaloneReactionAnimation = DirectAnimatedStickerNode()
effectiveScale = 1.4
} else {
standaloneReactionAnimation = LottieMetalAnimatedStickerNode()
}*/
#endif
standaloneReactionAnimation.isUserInteractionEnabled = false
let effectSize = CGSize(width: 380.0, height: 380.0)
var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self))
effectFrame.origin.x -= effectFrame.width * 0.3
self.standaloneReactionAnimation = standaloneReactionAnimation
standaloneReactionAnimation.frame = effectFrame
standaloneReactionAnimation.updateLayout(size: effectFrame.size)
self.addSubnode(standaloneReactionAnimation)
let pathPrefix = component.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(customEffectResource.id))
let source = AnimatedStickerResourceSource(account: component.context.account, resource: customEffectResource, fitzModifier: nil)
standaloneReactionAnimation.setup(source: source, width: Int(effectSize.width * effectiveScale), height: Int(effectSize.height * effectiveScale), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
standaloneReactionAnimation.completed = { [weak self, weak standaloneReactionAnimation] _ in
guard let self else {
return
}
if let standaloneReactionAnimation {
standaloneReactionAnimation.removeFromSupernode()
if self.standaloneReactionAnimation === standaloneReactionAnimation {
self.standaloneReactionAnimation = nil
}
}
}
standaloneReactionAnimation.visibility = true
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
self.endEditing(true)
})
}))
}
reactionContextNode.premiumReactionsSelected = { [weak self] _ in
guard let self, let component = self.component else {
return
}
if let controller = self.environment?.controller() {
controller.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
}
return true
}
}
let presentationData = component.updatedPresentationData?.initial ?? component.context.sharedContext.currentPresentationData.with({ $0 })
self.environment?.controller()?.present(UndoOverlayController(
presentationData: presentationData,
content: .premiumPaywall(
title: nil,
text: presentationData.strings.Chat_SendMessageMenu_ToastPremiumRequired_Text,
customUndoText: nil,
timeout: nil,
linkAction: nil
),
elevatedLayout: false,
action: { [weak self] action in
guard let self, let component = self.component else {
return false
}
if case .info = action {
self.window?.endEditing(true)
self.animateOutToEmpty = true
self.environment?.controller()?.dismiss()
let premiumController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .messageEffects, forceDark: false, dismissed: nil)
component.openPremiumPaywall(premiumController)
}
return false
}
), in: .current)
}
reactionContextNode.displayTail = true
reactionContextNode.forceTailToRight = false
reactionContextNode.forceDark = false
reactionContextNode.isMessageEffects = true
self.reactionContextNode = reactionContextNode
self.addSubview(reactionContextNode.view)
}
}
let sendButtonSize: CGSize
if component.sourceSendButton is ContextExtractedContentContainingView {
sendButtonSize = sourceSendButtonFrame.size
} else {
sendButtonSize = CGSize(width: min(sourceSendButtonFrame.width, 40.0), height: sourceSendButtonFrame.height)
}
var readySendButtonFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.maxX - sendButtonSize.width, y: sourceSendButtonFrame.minY), size: sendButtonSize)
var sourceActionsStackFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - 8.0 - actionsStackSize.width, y: sourceMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
if !isMessageVisible {
sourceActionsStackFrame.origin.y = sourceSendButtonFrame.maxY - sourceActionsStackFrame.height - 5.0
}
var readyMessageItemFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 0.0 - messageItemSize.width, y: readySendButtonFrame.maxY - 6.0 - messageItemSize.height), size: messageItemSize)
if let mediaPreview {
switch mediaPreview.layoutType {
case .message, .media:
break
case .videoMessage:
readyMessageItemFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - messageItemSize.width) * 0.5), y: readySendButtonFrame.maxY - 6.0 - messageItemSize.height), size: messageItemSize)
}
}
var readyActionsStackFrame: CGRect
if component.sourceSendButton is ContextExtractedContentContainingView {
readyActionsStackFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - 8.0 - actionsStackSize.width, y: readyMessageItemFrame.maxY + messageActionsSpacing + 4.0), size: actionsStackSize)
} else {
readyActionsStackFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - actionsStackSize.width, y: readyMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
}
if !isMessageVisible {
if component.sourceSendButton is ContextExtractedContentContainingView {
readyActionsStackFrame.origin.y = readySendButtonFrame.maxY - readyActionsStackFrame.height
} else {
readyActionsStackFrame.origin.y = readySendButtonFrame.maxY - readyActionsStackFrame.height - 5.0
}
}
let bottomOverflow = readyActionsStackFrame.maxY - (availableSize.height - environment.safeInsets.bottom)
if bottomOverflow > 0.0 {
readyMessageItemFrame.origin.y -= bottomOverflow
readyActionsStackFrame.origin.y -= bottomOverflow
if !(component.sourceSendButton is ContextExtractedContentContainingView) {
readySendButtonFrame.origin.y -= bottomOverflow
}
}
let inputCoverOverflow = readyMessageItemFrame.maxY + 7.0 - (availableSize.height - environment.inputHeight)
if inputCoverOverflow > 0.0 {
readyMessageItemFrame.origin.y -= inputCoverOverflow
readyActionsStackFrame.origin.y -= inputCoverOverflow
if !(component.sourceSendButton is ContextExtractedContentContainingView) {
readySendButtonFrame.origin.y -= inputCoverOverflow
}
}
if let mediaPreview {
switch mediaPreview.layoutType {
case .message, .media:
break
case .videoMessage:
let buttonActionsOffset: CGFloat = 5.0
let actionsStackAdjustmentY = sourceSendButtonFrame.maxY - buttonActionsOffset - readyActionsStackFrame.maxY
if abs(actionsStackAdjustmentY) < 10.0 {
readyActionsStackFrame.origin.y += actionsStackAdjustmentY
readyMessageItemFrame.origin.y += actionsStackAdjustmentY
}
readySendButtonFrame.origin.y = readyActionsStackFrame.maxY + buttonActionsOffset - readySendButtonFrame.height
}
}
let messageItemFrame: CGRect
let actionsStackFrame: CGRect
let sendButtonFrame: CGRect
switch self.presentationAnimationState {
case .initial:
if mediaPreview != nil {
messageItemFrame = readyMessageItemFrame
actionsStackFrame = readyActionsStackFrame
} else {
messageItemFrame = sourceMessageItemFrame
actionsStackFrame = sourceActionsStackFrame
}
sendButtonFrame = sourceSendButtonFrame
case .animatedOut:
if self.animateOutToEmpty {
messageItemFrame = readyMessageItemFrame
actionsStackFrame = readyActionsStackFrame
sendButtonFrame = readySendButtonFrame
} else {
if mediaPreview != nil {
messageItemFrame = readyMessageItemFrame
actionsStackFrame = readyActionsStackFrame
} else {
messageItemFrame = sourceMessageItemFrame
actionsStackFrame = sourceActionsStackFrame
}
sendButtonFrame = sourceSendButtonFrame
}
case .animatedIn:
messageItemFrame = readyMessageItemFrame
actionsStackFrame = readyActionsStackFrame
sendButtonFrame = readySendButtonFrame
}
transition.setFrame(view: messageItemView, frame: messageItemFrame)
transition.setAlpha(view: messageItemView, alpha: isMessageVisible ? 1.0 : 0.0)
messageItemView.updateClippingRect(
sourceMediaPreview: mediaPreview,
isAnimatedIn: self.presentationAnimationState.key == .animatedIn,
localFrame: messageItemFrame,
containerSize: availableSize,
transition: transition
)
transition.setPosition(view: actionsStackNode.view, position: CGPoint(x: actionsStackFrame.minX + actionsStackNode.layer.anchorPoint.x * actionsStackFrame.width, y: actionsStackFrame.minY + actionsStackNode.layer.anchorPoint.y * actionsStackFrame.height))
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)
ComponentTransition.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)
messageItemView.animateIn(
sourceTextInputView: component.textInputView as? ChatInputTextView,
isEditMessage: isEditMessage,
transition: transition
)
case .animatedOut:
transition.setAlpha(view: actionsStackNode.view, alpha: 0.0)
transition.setScale(view: actionsStackNode.view, scale: 0.001)
messageItemView.animateOut(
sourceTextInputView: component.textInputView as? ChatInputTextView,
toEmpty: self.animateOutToEmpty,
isEditMessage: isEditMessage,
transition: transition
)
}
} 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 standaloneReactionAnimation, let targetView = messageItemView.effectIconView {
let effectSize = CGSize(width: 380.0, height: 380.0)
var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self))
effectFrame.origin.x -= effectFrame.width * 0.3
transition.setFrame(view: standaloneReactionAnimation.view, frame: effectFrame)
}
if let reactionContextNode = self.reactionContextNode {
let reactionContextY = environment.statusBarHeight
let size = availableSize
var reactionsAnchorRect = messageItemFrame
if let mediaPreview {
switch mediaPreview.layoutType {
case .message, .media:
reactionsAnchorRect.size.width += 100.0
reactionsAnchorRect.origin.x -= 4.0
case .videoMessage:
reactionsAnchorRect.size.width += 100.0
reactionsAnchorRect.origin.x = reactionsAnchorRect.midX - 130.0
}
}
reactionsAnchorRect.origin.y -= reactionContextY
var isIntersectingContent = false
if reactionContextNode.isExpanded {
if messageItemFrame.minY <= reactionContextY + 50.0 + 4.0 {
isIntersectingContent = true
}
} else {
if messageItemFrame.minY <= reactionContextY + 300.0 + 4.0 {
isIntersectingContent = true
}
}
transition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: reactionContextY), size: CGSize(width: availableSize.width, height: availableSize.height - reactionContextY)))
reactionContextNode.updateLayout(size: size, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: false, isCoveredByInput: false, isAnimatingOut: false, transition: transition.containedViewLayoutTransition)
reactionContextNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: transition.containedViewLayoutTransition)
if self.presentationAnimationState.key == .animatedIn && previousAnimationState.key == .initial {
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()
})
}
}
sendButton.update(
context: component.context,
presentationData: presentationData,
backgroundNode: component.wallpaperBackgroundNode,
sourceSendButton: component.sourceSendButton,
isAnimatedIn: self.presentationAnimationState.key == .animatedIn,
isLoadingEffectAnimation: self.isLoadingEffectAnimation,
size: sendButtonFrame.size,
transition: transition
)
transition.setPosition(view: sendButton, position: sendButtonFrame.center)
transition.setBounds(view: sendButton, bounds: CGRect(origin: CGPoint(), size: sendButtonFrame.size))
transition.setScale(view: sendButton, scale: sendButtonScale)
sendButton.updateGlobalRect(rect: sendButtonFrame, within: availableSize, transition: transition)
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:
if previousAnimationState.key == .initial {
if environment.inputHeight != 0.0 {
if self.initializationDisplayLink == nil {
self.initializationDisplayLink = SharedDisplayLinkDriver.shared.add({ [weak self] _ in
guard let self else {
return
}
self.initializationDisplayLink?.invalidate()
self.initializationDisplayLink = nil
guard let component = self.component else {
return
}
if mediaPreview == nil {
component.textInputView?.isHidden = true
}
component.sourceSendButton.isHidden = true
})
}
} else {
if mediaPreview == nil {
component.textInputView?.isHidden = true
}
component.sourceSendButton.isHidden = true
}
}
backgroundAlpha = 1.0
case .animatedOut:
backgroundAlpha = 0.0
if self.animateOutToEmpty {
if mediaPreview == nil {
component.textInputView?.isHidden = false
}
component.sourceSendButton.isHidden = false
transition.setAlpha(view: sendButton, alpha: 0.0)
if let messageItemView = self.messageItemView, isMessageVisible {
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 !self.performedActionsOnAnimateOut {
self.performedActionsOnAnimateOut = true
if let component = self.component, !self.animateOutToEmpty {
if mediaPreview == nil {
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: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class ChatSendMessageContextScreen: ViewControllerComponentContainer, ChatSendMessageActionSheetController {
public final class InitialData {
fileprivate let messageEffect: AvailableMessageEffects.MessageEffect?
init(messageEffect: AvailableMessageEffects.MessageEffect?) {
self.messageEffect = messageEffect
}
}
private let context: AccountContext
private var processedDidAppear: Bool = false
private var processedDidDisappear: Bool = false
private var isActiveDisposable: Disposable?
override public var overlayWantsToBeBelowKeyboard: Bool {
if let componentView = self.node.hostView.componentView as? ChatSendMessageContextScreenComponent.View {
return componentView.wantsToBeBelowKeyboard()
} else {
return false
}
}
public init(
initialData: InitialData,
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
peerId: EnginePeer.Id?,
params: SendMessageActionSheetControllerParams,
hasEntityKeyboard: Bool,
gesture: ContextGesture?,
sourceSendButton: UIView,
textInputView: UITextView?,
emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?,
wallpaperBackgroundNode: WallpaperBackgroundNode?,
completion: @escaping () -> Void,
sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void,
schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void,
editPrice: @escaping (Int64) -> Void,
openPremiumPaywall: @escaping (ViewController) -> Void,
reactionItems: [ReactionItem]?,
availableMessageEffects: AvailableMessageEffects?,
isPremium: Bool,
richTextPreview: ChatSendMessageContextScreenRichTextPreview? = nil
) {
self.context = context
super.init(
context: context,
component: ChatSendMessageContextScreenComponent(
initialData: initialData,
context: context,
updatedPresentationData: updatedPresentationData,
peerId: peerId,
params: params,
hasEntityKeyboard: hasEntityKeyboard,
gesture: gesture,
sourceSendButton: sourceSendButton,
textInputView: textInputView,
emojiViewProvider: emojiViewProvider,
wallpaperBackgroundNode: wallpaperBackgroundNode,
completion: completion,
sendMessage: sendMessage,
schedule: schedule,
editPrice: editPrice,
openPremiumPaywall: openPremiumPaywall,
reactionItems: reactionItems,
availableMessageEffects: availableMessageEffects,
isPremium: isPremium,
richTextPreview: richTextPreview
),
navigationBarAppearance: .none,
statusBarStyle: .none,
presentationMode: .default,
updatedPresentationData: updatedPresentationData
)
self.lockOrientation = true
self.blocksBackgroundWhenInOverlay = true
self.isActiveDisposable = (context.sharedContext.applicationBindings.applicationInForeground
|> filter { !$0 }
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] _ in
guard let self else {
return
}
self.dismiss()
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.isActiveDisposable?.dispose()
}
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)
}
}
}
public static func initialData(context: AccountContext, currentMessageEffectId: Int64?) -> Signal<InitialData, NoError> {
let messageEffect: Signal<AvailableMessageEffects.MessageEffect?, NoError>
if let currentMessageEffectId {
messageEffect = 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 == currentMessageEffectId || messageEffect.effectSticker.fileId.id == currentMessageEffectId {
return messageEffect
}
}
return nil
}
} else {
messageEffect = .single(nil)
}
return messageEffect
|> map { messageEffect -> InitialData in
return InitialData(messageEffect: messageEffect)
}
}
}