Swiftgram/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift
Ilya Laktyushin e5762bd9c8 Various fixes
2025-05-18 04:35:56 +04:00

1560 lines
78 KiB
Swift

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
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
}
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: ASDisplayNode
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
init(
initialData: ChatSendMessageContextScreen.InitialData,
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
peerId: EnginePeer.Id?,
params: SendMessageActionSheetControllerParams,
hasEntityKeyboard: Bool,
gesture: ContextGesture,
sourceSendButton: ASDisplayNode,
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
) {
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
}
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.view, 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.view, to: self)
self.stableSourceSendButtonFrame = sourceSendButtonFrame
default:
if let stableSourceSendButtonFrame = self.stableSourceSendButtonFrame {
sourceSendButtonFrame = stableSourceSendButtonFrame
} else {
sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
self.stableSourceSendButtonFrame = sourceSendButtonFrame
}
}
let sendButtonScale: CGFloat
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
}
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(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: ContextControllerActionsListStackItem(
id: AnyHashable("items"),
items: items,
reactionItems: nil,
previewReaction: nil,
tip: nil,
tipSignal: .single(nil),
dismissed: nil
), animated: !transition.animation.isImmediate)
} else {
actionsStackNode = ContextControllerActionsStackNode(
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: ContextControllerActionsListStackItem(
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 = 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 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
}
let messageItemSize = messageItemView.update(
context: component.context,
presentationData: presentationData,
backgroundNode: wallpaperBackgroundNode,
textString: textString,
sourceTextInputView: component.textInputView as? ChatInputTextView,
emojiViewProvider: component.emojiViewProvider,
sourceMediaPreview: mediaPreview,
mediaCaptionIsAbove: self.mediaCaptionIsAbove,
textInsets: messageTextInsets,
explicitBackgroundSize: explicitMessageBackgroundSize,
maxTextWidth: 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, MediaResource)?
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.account.postbox.mediaBox.resourceStatus(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.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(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(width: min(sourceSendButtonFrame.width, 44.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 - 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 + 8.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(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - actionsStackSize.width, y: readyMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
if !isMessageVisible {
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
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
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: ASDisplayNode,
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
) {
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
),
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)
}
}
}