mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-04-07 21:57:51 +00:00
Fixes
fix localeWithStrings globally (#30)
Fix badge on zoomed devices. closes #9
Hide channel bottom panel closes #27
Another attempt to fix badge on some Zoomed devices
Force System Share sheet tg://sg/debug
fixes for device badge
New Crowdin updates (#34)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
Fix input panel hidden on selection (#31)
* added if check for selectionState != nil
* same order of subnodes
Revert "Fix input panel hidden on selection (#31)"
This reverts commit e8a8bb1496.
Fix input panel for channels Closes #37
Quickly share links with system's share menu
force tabbar when editing
increase height for correct animation
New translations sglocalizable.strings (Ukrainian) (#38)
Hide Post Story button
Fix 10.15.1
Fix archive option for long-tap
Enable in-app Safari
Disable some unsupported purchases
disableDeleteChatSwipeOption + refactor restart alert
Hide bot in suggestions list
Fix merge v11.0
Fix exceptions for safari webview controller
New Crowdin updates (#47)
* New translations sglocalizable.strings (Romanian)
* New translations sglocalizable.strings (French)
* New translations sglocalizable.strings (Spanish)
* New translations sglocalizable.strings (Afrikaans)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Catalan)
* New translations sglocalizable.strings (Czech)
* New translations sglocalizable.strings (Danish)
* New translations sglocalizable.strings (German)
* New translations sglocalizable.strings (Greek)
* New translations sglocalizable.strings (Finnish)
* New translations sglocalizable.strings (Hebrew)
* New translations sglocalizable.strings (Hungarian)
* New translations sglocalizable.strings (Italian)
* New translations sglocalizable.strings (Japanese)
* New translations sglocalizable.strings (Korean)
* New translations sglocalizable.strings (Dutch)
* New translations sglocalizable.strings (Norwegian)
* New translations sglocalizable.strings (Polish)
* New translations sglocalizable.strings (Portuguese)
* New translations sglocalizable.strings (Serbian (Cyrillic))
* New translations sglocalizable.strings (Swedish)
* New translations sglocalizable.strings (Turkish)
* New translations sglocalizable.strings (Vietnamese)
* New translations sglocalizable.strings (Indonesian)
* New translations sglocalizable.strings (Hindi)
* New translations sglocalizable.strings (Uzbek)
New Crowdin updates (#49)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Arabic)
New translations sglocalizable.strings (Russian) (#51)
Call confirmation
WIP Settings search
Settings Search
Localize placeholder
Update AccountUtils.swift
mark mutual contact
Align back context action to left
New Crowdin updates (#54)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Ukrainian)
Independent Playground app for simulator
New translations sglocalizable.strings (Ukrainian) (#55)
Playground UIKit base and controllers
Inject SwiftUI view with overflow to AsyncDisplayKit
Launch Playgound project on simulator
Create .swiftformat
Move Playground to example
Update .swiftformat
Init SwiftUIViewController
wip
New translations sglocalizable.strings (Chinese Traditional) (#57)
Xcode 16 fixes
Fix
New translations sglocalizable.strings (Italian) (#59)
New translations sglocalizable.strings (Chinese Simplified) (#63)
Force disable CallKit integration due to missing NSE Entitlement
Fix merge
Fix whole chat translator
Sweetpad config
Bump version
11.3.1 fixes
Mutual contact placement fix
Disable Video PIP swipe
Update versions.json
Fix PIP crash
1628 lines
83 KiB
Swift
1628 lines
83 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 sgTranslationContext: (outgoingMessageTranslateToLang: String?, translate: (() -> Void)?, changeTranslationLanguage: (() -> ())?)
|
|
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
|
|
// MARK: Swiftgram
|
|
init(
|
|
sgTranslationContext: (outgoingMessageTranslateToLang: String?, translate: (() -> Void)?, changeTranslationLanguage: (() -> ())?) = (outgoingMessageTranslateToLang: nil, translate: nil, changeTranslationLanguage: nil),
|
|
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.sgTranslationContext = sgTranslationContext
|
|
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
|
|
}
|
|
|
|
actionsStackNode.highlightGestureMoved(location: actionsStackNode.view.convert(location, from: view))
|
|
}
|
|
component.gesture.externalEnded = { [weak self] viewAndLocation in
|
|
guard let self, let actionsStackNode = self.actionsStackNode else {
|
|
return
|
|
}
|
|
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
|
|
}
|
|
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()
|
|
}
|
|
)))
|
|
}
|
|
|
|
// MARK: Swiftgram
|
|
if !isSecret {
|
|
if let outgoingMessageTranslateToLang = component.sgTranslationContext.outgoingMessageTranslateToLang {
|
|
var languageCode = presentationData.strings.baseLanguageCode
|
|
let rawSuffix = "-raw"
|
|
if languageCode.hasSuffix(rawSuffix) {
|
|
languageCode = String(languageCode.dropLast(rawSuffix.count))
|
|
}
|
|
|
|
// Assuming, user want to send message in the same language the chat is
|
|
let toLang = outgoingMessageTranslateToLang
|
|
let key = "Translation.Language.\(toLang)"
|
|
let translateTitle: String
|
|
if let string = presentationData.strings.primaryComponent.dict[key] {
|
|
translateTitle = presentationData.strings.Conversation_Translation_TranslateTo(string).string
|
|
} else {
|
|
let languageLocale = Locale(identifier: languageCode)
|
|
let toLanguage = languageLocale.localizedString(forLanguageCode: toLang) ?? ""
|
|
translateTitle = presentationData.strings.Conversation_Translation_TranslateToOther(toLanguage).string
|
|
}
|
|
|
|
items.append(.action(ContextMenuActionItem(
|
|
id: AnyHashable("sgTranslate"),
|
|
text: translateTitle,
|
|
icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.contextMenu.primaryColor)
|
|
}, action: { [weak self] _, _ in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
self.animateOutToEmpty = true
|
|
|
|
component.sgTranslationContext.translate?()
|
|
self.environment?.controller()?.dismiss()
|
|
}
|
|
)))
|
|
|
|
items.append(.action(ContextMenuActionItem(
|
|
id: AnyHashable("sgChangeTranslateLang"),
|
|
text: presentationData.strings.Translate_ChangeLanguage,
|
|
icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Caption"), color: theme.contextMenu.primaryColor)
|
|
}, action: { [weak self] _, _ in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
self.animateOutToEmpty = true
|
|
|
|
self.environment?.controller()?.dismiss()
|
|
component.sgTranslationContext.changeTranslationLanguage?()
|
|
}
|
|
)))
|
|
|
|
} else {
|
|
items.append(.action(ContextMenuActionItem(
|
|
id: AnyHashable("sgChangeTranslateLang"),
|
|
text: presentationData.strings.Conversation_Translation_TranslateToOther("...").string,
|
|
icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Caption"), color: theme.contextMenu.primaryColor)
|
|
}, action: { [weak self] _, _ in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
self.animateOutToEmpty = true
|
|
|
|
self.environment?.controller()?.dismiss()
|
|
component.sgTranslationContext.changeTranslationLanguage?()
|
|
}
|
|
)))
|
|
}
|
|
}
|
|
|
|
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(
|
|
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 {
|
|
customEffectResource = (FileMediaReference.standalone(media: effectAnimation), effectAnimation.resource)
|
|
} else {
|
|
let effectSticker = messageEffect.effectSticker
|
|
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(
|
|
sgTranslationContext: (outgoingMessageTranslateToLang: String?, translate: (() -> Void)?, changeTranslationLanguage: (() -> ())?) = (outgoingMessageTranslateToLang: nil, translate: nil, changeTranslationLanguage: nil),
|
|
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(
|
|
sgTranslationContext: sgTranslationContext,
|
|
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)
|
|
}
|
|
}
|
|
}
|