Swiftgram/submodules/TelegramUI/Sources/ChatController.swift
2024-07-02 08:47:03 +04:00

10256 lines
584 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import UIKit
import Postbox
import SwiftSignalKit
import Display
import AsyncDisplayKit
import TelegramCore
import SafariServices
import MobileCoreServices
import Intents
import LegacyComponents
import TelegramPresentationData
import TelegramUIPreferences
import DeviceAccess
import TextFormat
import TelegramBaseController
import AccountContext
import TelegramStringFormatting
import OverlayStatusController
import DeviceLocationManager
import ShareController
import UrlEscaping
import ContextUI
import ComposePollUI
import AlertUI
import PresentationDataUtils
import UndoUI
import TelegramCallsUI
import TelegramNotices
import GameUI
import ScreenCaptureDetection
import GalleryUI
import OpenInExternalAppUI
import LegacyUI
import InstantPageUI
import LocationUI
import BotPaymentsUI
import DeleteChatPeerActionSheetItem
import HashtagSearchUI
import LegacyMediaPickerUI
import Emoji
import PeerAvatarGalleryUI
import PeerInfoUI
import RaiseToListen
import UrlHandling
import AvatarNode
import AppBundle
import LocalizedPeerData
import PhoneNumberFormat
import SettingsUI
import UrlWhitelist
import TelegramIntents
import TooltipUI
import StatisticsUI
import MediaResources
import GalleryData
import ChatInterfaceState
import InviteLinksUI
import Markdown
import TelegramPermissionsUI
import Speak
import TranslateUI
import UniversalMediaPlayer
import WallpaperBackgroundNode
import ChatListUI
import CalendarMessageScreen
import ReactionSelectionNode
import ReactionListContextMenuContent
import AttachmentUI
import AttachmentTextInputPanelNode
import MediaPickerUI
import ChatPresentationInterfaceState
import Pasteboard
import ChatSendMessageActionUI
import ChatTextLinkEditUI
import WebUI
import PremiumUI
import ImageTransparency
import StickerPackPreviewUI
import TextNodeWithEntities
import EntityKeyboard
import ChatTitleView
import EmojiStatusComponent
import ChatTimerScreen
import MediaPasteboardUI
import ChatListHeaderComponent
import ChatControllerInteraction
import FeaturedStickersScreen
import ChatEntityKeyboardInputNode
import StorageUsageScreen
import AvatarEditorScreen
import ChatScheduleTimeController
import ICloudResources
import StoryContainerScreen
import MoreHeaderButton
import VolumeButtons
import ChatAvatarNavigationNode
import ChatContextQuery
import PeerReportScreen
import PeerSelectionController
import SaveToCameraRoll
import ChatMessageDateAndStatusNode
import ReplyAccessoryPanelNode
import TextSelectionNode
import ChatMessagePollBubbleContentNode
import ChatMessageItem
import ChatMessageItemImpl
import ChatMessageItemView
import ChatMessageItemCommon
import ChatMessageAnimatedStickerItemNode
import ChatMessageBubbleItemNode
import ChatNavigationButton
import WebsiteType
import ChatQrCodeScreen
import PeerInfoScreen
import MediaEditor
import MediaEditorScreen
import WallpaperGalleryScreen
import WallpaperGridScreen
import VideoMessageCameraScreen
import TopMessageReactions
import AudioWaveform
import PeerNameColorScreen
import ChatEmptyNode
import ChatMediaInputStickerGridItem
import AdsInfoScreen
import MessageUI
import PhoneNumberFormat
import OwnershipTransferController
import OldChannelsController
public enum ChatControllerPeekActions {
case standard
case remove(() -> Void)
}
public final class ChatControllerOverlayPresentationData {
public let expandData: (ASDisplayNode?, () -> Void)
public init(expandData: (ASDisplayNode?, () -> Void)) {
self.expandData = expandData
}
}
enum ChatLocationInfoData {
case peer(Promise<PeerView>)
case replyThread(Promise<Message?>)
case customChatContents
}
enum ChatRecordingActivity {
case voice
case instantVideo
case none
}
public enum NavigateToMessageLocation {
case id(MessageId, NavigateToMessageParams)
case index(MessageIndex)
case upperBound(PeerId)
var messageId: MessageId? {
switch self {
case let .id(id, _):
return id
case let .index(index):
return index.id
case .upperBound:
return nil
}
}
var peerId: PeerId {
switch self {
case let .id(id, _):
return id.peerId
case let .index(index):
return index.id.peerId
case let .upperBound(peerId):
return peerId
}
}
}
func isTopmostChatController(_ controller: ChatControllerImpl) -> Bool {
if let _ = controller.navigationController {
var hasOther = false
controller.window?.forEachController({ c in
if c is ChatControllerImpl {
if controller !== c {
hasOther = true
} else {
hasOther = false
}
}
})
if hasOther {
return false
}
}
return true
}
func calculateSlowmodeActiveUntilTimestamp(account: Account, untilTimestamp: Int32?) -> Int32? {
guard let untilTimestamp = untilTimestamp else {
return nil
}
let timestamp = Int32(Date().timeIntervalSince1970)
let remainingTime = max(0, untilTimestamp - timestamp)
if remainingTime == 0 {
return nil
} else {
return untilTimestamp
}
}
struct ScrolledToMessageId: Equatable {
struct AllowedReplacementDirections: OptionSet {
var rawValue: Int32
static let up = AllowedReplacementDirections(rawValue: 1 << 0)
static let down = AllowedReplacementDirections(rawValue: 1 << 1)
}
var id: MessageId
var allowedReplacementDirection: AllowedReplacementDirections
}
public final class ChatControllerImpl: TelegramBaseController, ChatController, GalleryHiddenMediaTarget, UIDropInteractionDelegate {
var validLayout: ContainerViewLayout?
public weak var parentController: ViewController?
public weak var customNavigationController: NavigationController?
let currentChatListFilter: Int32?
let chatNavigationStack: [ChatNavigationStackItem]
let customChatNavigationStack: [EnginePeer.Id]?
public var peekActions: ChatControllerPeekActions = .standard
var didSetup3dTouch: Bool = false
let context: AccountContext
public let chatLocation: ChatLocation
public let subject: ChatControllerSubject?
var botStart: ChatControllerInitialBotStart?
var attachBotStart: ChatControllerInitialAttachBotStart?
var botAppStart: ChatControllerInitialBotAppStart?
let peerDisposable = MetaDisposable()
let titleDisposable = MetaDisposable()
var accountPeerDisposable: Disposable?
let navigationActionDisposable = MetaDisposable()
var networkStateDisposable: Disposable?
let messageIndexDisposable = MetaDisposable()
let _chatLocationInfoReady = Promise<Bool>()
var didSetChatLocationInfoReady = false
let chatLocationInfoData: ChatLocationInfoData
let cachedDataReady = Promise<Bool>()
var didSetCachedDataReady = false
let wallpaperReady = Promise<Bool>()
let presentationReady = Promise<Bool>()
var presentationInterfaceState: ChatPresentationInterfaceState
let presentationInterfaceStatePromise: ValuePromise<ChatPresentationInterfaceState>
public var presentationInterfaceStateSignal: Signal<Any, NoError> {
return self.presentationInterfaceStatePromise.get() |> map { $0 }
}
public var selectedMessageIds: Set<EngineMessage.Id>? {
return self.presentationInterfaceState.interfaceState.selectionState?.selectedIds
}
let chatThemeEmoticonPromise = Promise<String?>()
let chatWallpaperPromise = Promise<TelegramWallpaper?>()
var chatTitleView: ChatTitleView?
var leftNavigationButton: ChatNavigationButton?
var rightNavigationButton: ChatNavigationButton?
var secondaryRightNavigationButton: ChatNavigationButton?
var chatInfoNavigationButton: ChatNavigationButton?
var moreBarButton: MoreHeaderButton
var moreInfoNavigationButton: ChatNavigationButton?
var peerView: PeerView?
var threadInfo: EngineMessageHistoryThread.Info?
var historyStateDisposable: Disposable?
let galleryHiddenMesageAndMediaDisposable = MetaDisposable()
let temporaryHiddenGalleryMediaDisposable = MetaDisposable()
let chatBackgroundNode: WallpaperBackgroundNode
private(set) var controllerInteraction: ChatControllerInteraction?
var interfaceInteraction: ChatPanelInterfaceInteraction?
let messageContextDisposable = MetaDisposable()
let controllerNavigationDisposable = MetaDisposable()
let sentMessageEventsDisposable = MetaDisposable()
let failedMessageEventsDisposable = MetaDisposable()
let sentPeerMediaMessageEventsDisposable = MetaDisposable()
weak var currentFailedMessagesAlertController: ViewController?
let messageActionCallbackDisposable = MetaDisposable()
let messageActionUrlAuthDisposable = MetaDisposable()
let editMessageDisposable = MetaDisposable()
let editMessageErrorsDisposable = MetaDisposable()
let enqueueMediaMessageDisposable = MetaDisposable()
var resolvePeerByNameDisposable: MetaDisposable?
var shareStatusDisposable: MetaDisposable?
var clearCacheDisposable: MetaDisposable?
var bankCardDisposable: MetaDisposable?
var hasActiveGroupCallDisposable: Disposable?
var sendAsPeersDisposable: Disposable?
var preloadAttachBotIconsDisposables: DisposableSet?
var keepMessageCountersSyncrhonizedDisposable: Disposable?
var keepSavedMessagesSyncrhonizedDisposable: Disposable?
var saveMediaDisposable: MetaDisposable?
var giveawayStatusDisposable: MetaDisposable?
var nameColorDisposable: Disposable?
let editingMessage = ValuePromise<Float?>(nil, ignoreRepeated: true)
let startingBot = ValuePromise<Bool>(false, ignoreRepeated: true)
let unblockingPeer = ValuePromise<Bool>(false, ignoreRepeated: true)
public let searching = ValuePromise<Bool>(false, ignoreRepeated: true)
let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>()
let loadingMessage = Promise<ChatLoadingMessageSubject?>(nil)
let performingInlineSearch = ValuePromise<Bool>(false, ignoreRepeated: true)
var stateServiceTasks: [AnyHashable: Disposable] = [:]
var preloadHistoryPeerId: PeerId?
let preloadHistoryPeerIdDisposable = MetaDisposable()
var preloadNextChatPeerId: PeerId?
let preloadNextChatPeerIdDisposable = MetaDisposable()
var preloadSavedMessagesChatsDisposable: Disposable?
let botCallbackAlertMessage = Promise<String?>(nil)
var botCallbackAlertMessageDisposable: Disposable?
var selectMessagePollOptionDisposables: DisposableDict<MessageId>?
var selectPollOptionFeedback: HapticFeedback?
var resolveUrlDisposable: MetaDisposable?
var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
var searchQuerySuggestionState: (ChatPresentationInputQuery?, Disposable)?
var urlPreviewQueryState: (UrlPreviewState?, Disposable)?
var editingUrlPreviewQueryState: (UrlPreviewState?, Disposable)?
var replyMessageState: (EngineMessage.Id, Disposable)?
var searchState: ChatSearchState?
var shakeFeedback: HapticFeedback?
var recordingModeFeedback: HapticFeedback?
var recorderFeedback: HapticFeedback?
var audioRecorderValue: ManagedAudioRecorder?
var audioRecorder = Promise<ManagedAudioRecorder?>()
var audioRecorderDisposable: Disposable?
var audioRecorderStatusDisposable: Disposable?
var videoRecorderValue: VideoMessageCameraScreen?
var videoRecorder = Promise<VideoMessageCameraScreen?>()
var videoRecorderDisposable: Disposable?
var recorderDataDisposable = MetaDisposable()
var buttonKeyboardMessageDisposable: Disposable?
var cachedDataDisposable: Disposable?
var chatUnreadCountDisposable: Disposable?
var buttonUnreadCountDisposable: Disposable?
var chatUnreadMentionCountDisposable: Disposable?
var peerInputActivitiesDisposable: Disposable?
var peerInputActivitiesPromise = Promise<[(Peer, PeerInputActivity)]>()
var interactiveEmojiSyncDisposable = MetaDisposable()
var recentlyUsedInlineBotsValue: [Peer] = []
var recentlyUsedInlineBotsDisposable: Disposable?
var unpinMessageDisposable: MetaDisposable?
let typingActivityPromise = Promise<Bool>(false)
var inputActivityDisposable: Disposable?
var recordingActivityValue: ChatRecordingActivity = .none
let recordingActivityPromise = ValuePromise<ChatRecordingActivity>(.none, ignoreRepeated: true)
var recordingActivityDisposable: Disposable?
var acquiredRecordingActivityDisposable: Disposable?
let choosingStickerActivityPromise = ValuePromise<Bool>(false)
var choosingStickerActivityDisposable: Disposable?
var searchDisposable: MetaDisposable?
var historyNavigationStack = ChatHistoryNavigationStack()
public let canReadHistory = ValuePromise<Bool>(true, ignoreRepeated: true)
var reminderActivity: NSUserActivity?
var isReminderActivityEnabled: Bool = false
var canReadHistoryValue = false
var canReadHistoryDisposable: Disposable?
var themeEmoticonAndDarkAppearancePreviewPromise = Promise<(String?, Bool?)>((nil, nil))
var didSetPresentationData = false
var presentationData: PresentationData
var presentationDataPromise = Promise<PresentationData>()
override public var updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) {
return (self.presentationData, self.presentationDataPromise.get())
}
var presentationDataDisposable: Disposable?
var forcedTheme: PresentationTheme?
var forcedNavigationBarTheme: PresentationTheme?
var forcedWallpaper: TelegramWallpaper?
var automaticMediaDownloadSettings: MediaAutoDownloadSettings
var automaticMediaDownloadSettingsDisposable: Disposable?
var disableStickerAnimationsPromise = ValuePromise<Bool>(false)
var disableStickerAnimationsValue = false
var disableStickerAnimations: Bool {
get {
return self.disableStickerAnimationsValue
} set {
self.disableStickerAnimationsPromise.set(newValue)
}
}
var stickerSettings: ChatInterfaceStickerSettings
var stickerSettingsDisposable: Disposable?
var applicationInForegroundDisposable: Disposable?
var applicationInFocusDisposable: Disposable?
let checksTooltipDisposable = MetaDisposable()
var shouldDisplayChecksTooltip = false
let peerSuggestionsDisposable = MetaDisposable()
let peerSuggestionsDismissDisposable = MetaDisposable()
var displayedConvertToGigagroupSuggestion = false
var checkedPeerChatServiceActions = false
var willAppear = false
var didAppear = false
var scheduledActivateInput: ChatControllerActivateInput?
var raiseToListen: RaiseToListenManager?
var voicePlaylistDidEndTimestamp: Double = 0.0
weak var emojiTooltipController: TooltipController?
weak var sendingOptionsTooltipController: TooltipController?
weak var searchResultsTooltipController: TooltipController?
weak var messageTooltipController: TooltipController?
weak var videoUnmuteTooltipController: TooltipController?
var didDisplayVideoUnmuteTooltip = false
var didDisplayGroupEmojiTip = false
var didDisplaySendWhenOnlineTip = false
let displaySendWhenOnlineTipDisposable = MetaDisposable()
weak var silentPostTooltipController: TooltipController?
weak var mediaRecordingModeTooltipController: TooltipController?
weak var mediaRestrictedTooltipController: TooltipController?
var mediaRestrictedTooltipControllerMode = true
weak var checksTooltipController: TooltipController?
weak var copyProtectionTooltipController: TooltipController?
weak var emojiPackTooltipController: TooltipScreen?
weak var birthdayTooltipController: TooltipScreen?
weak var slowmodeTooltipController: ChatSlowmodeHintController?
weak var currentContextController: ContextController?
public var visibleContextController: ViewController? {
return self.currentContextController
}
weak var sendMessageActionsController: ChatSendMessageActionSheetController?
var searchResultsController: ChatSearchResultsController?
weak var themeScreen: ChatThemeScreen?
weak var currentPinchController: PinchController?
weak var currentPinchSourceItemNode: ListViewItemNode?
var screenCaptureManager: ScreenCaptureDetectionManager?
let chatAdditionalDataDisposable = MetaDisposable()
var reportIrrelvantGeoNoticePromise = Promise<Bool?>()
var reportIrrelvantGeoNotice: Bool?
var reportIrrelvantGeoDisposable: Disposable?
var hasScheduledMessages: Bool = false
var volumeButtonsListener: VolumeButtonsListener?
var beginMediaRecordingRequestId: Int = 0
var lockMediaRecordingRequestId: Int?
var updateSlowmodeStatusDisposable = MetaDisposable()
var updateSlowmodeStatusTimerValue: Int32?
var isDismissed = false
var focusOnSearchAfterAppearance: (ChatSearchDomain, String)?
let keepPeerInfoScreenDataHotDisposable = MetaDisposable()
let preloadAvatarDisposable = MetaDisposable()
let peekData: ChatPeekTimeout?
let peekTimerDisposable = MetaDisposable()
let createVoiceChatDisposable = MetaDisposable()
let selectAddMemberDisposable = MetaDisposable()
let addMemberDisposable = MetaDisposable()
let joinChannelDisposable = MetaDisposable()
var shouldDisplayDownButton = false
var hasEmbeddedTitleContent = false
var isEmbeddedTitleContentHidden = false
let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
weak var attachmentController: AttachmentController?
weak var currentWebAppController: ViewController?
weak var currentImportMessageTooltip: UndoOverlayController?
public var customNavigationBarContentNode: NavigationBarContentNode?
public var customNavigationPanelNode: ChatControllerCustomNavigationPanelNode?
public var stateUpdated: ((ContainedViewLayoutTransition) -> Void)?
public var customDismissSearch: (() -> Void)?
public override var customData: Any? {
return self.chatLocation
}
override public var customNavigationData: CustomViewControllerNavigationData? {
get {
if let peerId = self.chatLocation.peerId {
return ChatControllerNavigationData(peerId: peerId, threadId: self.chatLocation.threadId)
} else {
return nil
}
}
}
override public var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? {
return .widthMultiplier(factor: 0.35, min: 16.0, max: 200.0)
}
var scheduledScrollToMessageId: (MessageId, NavigateToMessageParams)?
public var purposefulAction: (() -> Void)?
var updatedClosedPinnedMessageId: ((MessageId) -> Void)?
var requestedUnpinAllMessages: ((Int, MessageId) -> Void)?
public var isSelectingMessagesUpdated: ((Bool) -> Void)?
let scrolledToMessageId = ValuePromise<ScrolledToMessageId?>(nil, ignoreRepeated: true)
var scrolledToMessageIdValue: ScrolledToMessageId? = nil {
didSet {
self.scrolledToMessageId.set(self.scrolledToMessageIdValue)
}
}
var translationStateDisposable: Disposable?
var premiumGiftSuggestionDisposable: Disposable?
var nextChannelToReadDisposable: Disposable?
var offerNextChannelToRead = false
var inviteRequestsContext: PeerInvitationImportersContext?
var inviteRequestsDisposable = MetaDisposable()
var overlayTitle: String? {
var title: String?
if let threadInfo = self.threadInfo {
title = threadInfo.title
} else if let peerView = self.peerView {
if let peer = peerViewMainPeer(peerView) {
title = EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)
}
}
return title
}
var currentSpeechHolder: SpeechSynthesizerHolder?
var powerSavingMonitoringDisposable: Disposable?
var avatarNode: ChatAvatarNavigationNode?
var storyStats: PeerStoryStats?
var performTextSelectionAction: ((Message?, Bool, NSAttributedString, TextSelectionAction) -> Void)?
var performOpenURL: ((Message?, String, Promise<Bool>?) -> Void)?
var networkSpeedEventsDisposable: Disposable?
var stickerVideoExport: MediaEditorVideoExport?
var messageComposeController: MFMessageComposeViewController?
public var alwaysShowSearchResultsAsList: Bool = false {
didSet {
self.presentationInterfaceState = self.presentationInterfaceState.updatedDisplayHistoryFilterAsList(self.alwaysShowSearchResultsAsList)
self.chatDisplayNode.alwaysShowSearchResultsAsList = self.alwaysShowSearchResultsAsList
}
}
public var includeSavedPeersInSearchResults: Bool = false {
didSet {
self.chatDisplayNode.includeSavedPeersInSearchResults = self.includeSavedPeersInSearchResults
}
}
public var showListEmptyResults: Bool = false {
didSet {
self.chatDisplayNode.showListEmptyResults = self.showListEmptyResults
}
}
public init(
context: AccountContext,
chatLocation: ChatLocation,
chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil),
subject: ChatControllerSubject? = nil,
botStart: ChatControllerInitialBotStart? = nil,
attachBotStart: ChatControllerInitialAttachBotStart? = nil,
botAppStart: ChatControllerInitialBotAppStart? = nil,
mode: ChatControllerPresentationMode = .standard(.default),
peekData: ChatPeekTimeout? = nil,
peerNearbyData: ChatPeerNearbyData? = nil,
chatListFilter: Int32? = nil,
chatNavigationStack: [ChatNavigationStackItem] = [],
customChatNavigationStack: [EnginePeer.Id]? = nil,
params: ChatControllerParams? = nil
) {
let _ = ChatControllerCount.modify { value in
return value + 1
}
self.context = context
self.chatLocation = chatLocation
self.chatLocationContextHolder = chatLocationContextHolder
self.subject = subject
self.botStart = botStart
self.attachBotStart = attachBotStart
self.botAppStart = botAppStart
self.peekData = peekData
self.currentChatListFilter = chatListFilter
self.chatNavigationStack = chatNavigationStack
self.customChatNavigationStack = customChatNavigationStack
self.forcedTheme = params?.forcedTheme
self.forcedNavigationBarTheme = params?.forcedNavigationBarTheme
self.forcedWallpaper = params?.forcedWallpaper
var useSharedAnimationPhase = false
switch mode {
case .standard(.default):
useSharedAnimationPhase = true
default:
break
}
self.chatBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: useSharedAnimationPhase)
self.wallpaperReady.set(self.chatBackgroundNode.isReady)
var locationBroadcastPanelSource: LocationBroadcastPanelSource
var groupCallPanelSource: GroupCallPanelSource
switch chatLocation {
case let .peer(peerId):
locationBroadcastPanelSource = .peer(peerId)
switch subject {
case .message, .none:
groupCallPanelSource = .peer(peerId)
default:
groupCallPanelSource = .none
}
self.chatLocationInfoData = .peer(Promise())
case let .replyThread(replyThreadMessage):
locationBroadcastPanelSource = .none
groupCallPanelSource = .none
let promise = Promise<Message?>()
if let effectiveMessageId = replyThreadMessage.effectiveMessageId {
promise.set(context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: effectiveMessageId))
|> map { message -> Message? in
guard let message = message else {
return nil
}
return message._asMessage()
})
} else {
promise.set(.single(nil))
}
self.chatLocationInfoData = .replyThread(promise)
case .customChatContents:
locationBroadcastPanelSource = .none
groupCallPanelSource = .none
self.chatLocationInfoData = .customChatContents
}
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
if let forcedTheme = self.forcedTheme {
presentationData = presentationData.withUpdated(theme: forcedTheme)
}
if let forcedWallpaper = self.forcedWallpaper {
presentationData = presentationData.withUpdated(chatWallpaper: forcedWallpaper)
}
self.presentationData = presentationData
self.automaticMediaDownloadSettings = context.sharedContext.currentAutomaticMediaDownloadSettings
self.stickerSettings = ChatInterfaceStickerSettings()
self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: mode, chatLocation: chatLocation, subject: subject, peerNearbyData: peerNearbyData, greetingData: context.prefetchManager?.preloadedGreetingSticker, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil)
if case let .customChatContents(customChatContents) = subject {
switch customChatContents.kind {
case .quickReplyMessageInput:
break
case let .businessLinkSetup(link):
if !link.message.isEmpty {
self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState({ interfaceState in
return interfaceState.withUpdatedEffectiveInputState(ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(link.message, entities: link.entities)))
})
}
case .hashTagSearch:
break
}
}
self.presentationInterfaceStatePromise = ValuePromise(self.presentationInterfaceState)
var mediaAccessoryPanelVisibility = MediaAccessoryPanelVisibility.none
if case .standard = mode {
mediaAccessoryPanelVisibility = .specific(size: .compact)
} else {
locationBroadcastPanelSource = .none
groupCallPanelSource = .none
}
let navigationBarPresentationData: NavigationBarPresentationData?
switch mode {
case .inline, .standard(.embedded):
navigationBarPresentationData = nil
default:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: false)
}
self.moreBarButton = MoreHeaderButton(color: self.presentationData.theme.rootController.navigationBar.buttonColor)
self.moreBarButton.isUserInteractionEnabled = true
super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource, groupCallPanelSource: groupCallPanelSource)
self.automaticallyControlPresentationContextLayout = false
self.blocksBackgroundWhenInOverlay = true
self.acceptsFocusWhenInOverlay = true
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.ready.set(.never())
self.scrollToTop = { [weak self] in
guard let strongSelf = self, strongSelf.isNodeLoaded else {
return
}
if let attachmentController = strongSelf.attachmentController {
attachmentController.scrollToTop?()
} else {
strongSelf.chatDisplayNode.scrollToTop()
}
}
self.attemptNavigation = { [weak self] action in
guard let strongSelf = self else {
return true
}
if let _ = strongSelf.videoRecorderValue {
return false
}
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
if strongSelf.presentVoiceMessageDiscardAlert(action: action, performAction: false) {
return false
}
if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject {
switch customChatContents.kind {
case .hashTagSearch:
return true
case let .quickReplyMessageInput(_, shortcutType):
if let historyView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, historyView.entries.isEmpty {
let titleString: String
let textString: String
switch shortcutType {
case .generic:
titleString = strongSelf.presentationData.strings.QuickReply_ChatRemoveGeneric_Title
textString = strongSelf.presentationData.strings.QuickReply_ChatRemoveGeneric_Text
case .greeting:
titleString = strongSelf.presentationData.strings.QuickReply_ChatRemoveGreetingMessage_Title
textString = strongSelf.presentationData.strings.QuickReply_ChatRemoveGreetingMessage_Text
case .away:
titleString = strongSelf.presentationData.strings.QuickReply_ChatRemoveAwayMessage_Title
textString = strongSelf.presentationData.strings.QuickReply_ChatRemoveAwayMessage_Text
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: titleString, text: textString, actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.QuickReply_ChatRemoveGeneric_DeleteAction, action: { [weak strongSelf] in
strongSelf?.dismiss()
})
]), in: .window(.root))
return false
}
case let .businessLinkSetup(link):
var inputText = convertMarkdownToAttributes(strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText)
inputText = trimChatInputText(inputText)
let entities = generateChatInputTextEntities(inputText, generateLinks: false)
let message = inputText.string
if message != link.message || entities != link.entities {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Business_Links_AlertUnsavedText, actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Business_Links_AlertUnsavedAction, action: { [weak strongSelf] in
strongSelf?.dismiss()
})
]), in: .window(.root))
return false
}
}
}
return true
}
let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message, params in
guard let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) else {
return false
}
let mode = params.mode
let displayVoiceMessageDiscardAlert: () -> Bool = {
if strongSelf.presentVoiceMessageDiscardAlert(action: { [weak self] in
if let strongSelf = self {
Queue.mainQueue().after(0.1, {
let _ = strongSelf.controllerInteraction?.openMessage(message, params)
})
}
}, performAction: false) {
return false
}
return true
}
strongSelf.commitPurposefulAction()
strongSelf.dismissAllTooltips()
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
var openMessageByAction = false
var isLocation = false
for media in message.media {
if media is TelegramMediaMap {
if !displayVoiceMessageDiscardAlert() {
return false
}
isLocation = true
}
if let file = media as? TelegramMediaFile {
if file.isInstantVideo {
if strongSelf.chatDisplayNode.isInputViewFocused {
strongSelf.returnInputViewFocus = true
strongSelf.chatDisplayNode.dismissInput()
}
}
if file.isMusic || file.isVoice || file.isInstantVideo {
if !displayVoiceMessageDiscardAlert() {
return false
}
if (file.isVoice || file.isInstantVideo) && message.minAutoremoveOrClearTimeout == viewOnceTimeout {
strongSelf.openViewOnceMediaMessage(message)
return false
}
} else if file.isVideo {
if !displayVoiceMessageDiscardAlert() {
return false
}
}
}
if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first {
switch extendedMedia {
case .preview:
if displayVoiceMessageDiscardAlert() {
strongSelf.controllerInteraction?.openCheckoutOrReceipt(message.id, params)
return true
} else {
return false
}
case .full:
break
}
} else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia {
switch extendedMedia {
case .preview:
if displayVoiceMessageDiscardAlert() {
strongSelf.controllerInteraction?.openCheckoutOrReceipt(message.id, nil)
return true
} else {
return false
}
case .full:
break
}
} else if media is TelegramMediaGiveaway || media is TelegramMediaGiveawayResults {
let progress = params.progress
let presentationData = strongSelf.presentationData
var signal = strongSelf.context.engine.payments.premiumGiveawayInfo(peerId: message.id.peerId, messageId: message.id)
let disposable: MetaDisposable
if let current = strongSelf.giveawayStatusDisposable {
disposable = current
} else {
disposable = MetaDisposable()
strongSelf.giveawayStatusDisposable = disposable
}
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
if let progress {
progress.set(.single(true))
return ActionDisposable {
Queue.mainQueue().async() {
progress.set(.single(false))
}
}
} else {
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
self?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.25, queue: Queue.mainQueue())
let progressDisposable = progressSignal.startStrict()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
disposable.set((signal
|> deliverOnMainQueue).startStrict(next: { [weak self] info in
if let strongSelf = self, let info {
strongSelf.displayGiveawayStatusInfo(messageId: message.id, giveawayInfo: info)
}
}))
return true
} else if let action = media as? TelegramMediaAction {
if !displayVoiceMessageDiscardAlert() {
return false
}
switch action.action {
case .pinnedMessageUpdated, .gameScore, .setSameChatWallpaper, .giveawayResults, .customText:
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil)))
break
}
}
case let .photoUpdated(image):
openMessageByAction = image != nil
case .groupPhoneCall, .inviteToGroupPhoneCall:
if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall {
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: EngineGroupCallDescription(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title, scheduleTimestamp: activeCall.scheduleTimestamp, subscribedToScheduled: activeCall.subscribedToScheduled, isStream: activeCall.isStream))
} else {
var canManageGroupCalls = false
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel {
if channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls) {
canManageGroupCalls = true
}
} else if let group = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramGroup {
if case .creator = group.role {
canManageGroupCalls = true
} else if case let .admin(rights, _) = group.role {
if rights.rights.contains(.canManageCalls) {
canManageGroupCalls = true
}
}
}
if canManageGroupCalls {
let text: String
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info {
text = strongSelf.presentationData.strings.LiveStream_CreateNewVoiceChatText
} else {
text = strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatText
}
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatStartNow, action: {
if let strongSelf = self {
var dismissStatus: (() -> Void)?
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: {
dismissStatus?()
}))
dismissStatus = { [weak self, weak statusController] in
self?.createVoiceChatDisposable.set(nil)
statusController?.dismiss()
}
strongSelf.present(statusController, in: .window(.root))
strongSelf.createVoiceChatDisposable.set((strongSelf.context.engine.calls.createGroupCall(peerId: message.id.peerId, title: nil, scheduleDate: nil, isExternalStream: false)
|> deliverOnMainQueue).startStrict(next: { [weak self] info in
guard let strongSelf = self else {
return
}
strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: EngineGroupCallDescription(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribedToScheduled: info.subscribedToScheduled, isStream: info.isStream))
}, error: { [weak self] error in
dismissStatus?()
guard let strongSelf = self else {
return
}
let text: String
switch error {
case .generic, .scheduledTooLate:
text = strongSelf.presentationData.strings.Login_UnknownError
case .anonymousNotAllowed:
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
text = strongSelf.presentationData.strings.LiveStream_AnonymousDisabledAlertText
} else {
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
}
}
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, completed: {
dismissStatus?()
}))
}
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatSchedule, action: {
if let strongSelf = self {
strongSelf.context.scheduleGroupCall(peerId: message.id.peerId)
}
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root))
}
}
return true
case .messageAutoremoveTimeoutUpdated:
var canSetupAutoremoveTimeout = false
if let _ = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat {
canSetupAutoremoveTimeout = false
} else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup {
if !group.hasBannedPermission(.banChangeInfo) {
canSetupAutoremoveTimeout = true
}
} else if let user = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramUser {
if user.id != strongSelf.context.account.peerId && user.botInfo == nil {
canSetupAutoremoveTimeout = true
}
} else if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel {
if channel.hasPermission(.changeInfo) {
canSetupAutoremoveTimeout = true
}
}
if canSetupAutoremoveTimeout {
strongSelf.presentAutoremoveSetup()
}
case let .paymentSent(currency, _, _, _, _):
if currency == "XTR" {
let _ = (context.engine.payments.requestBotPaymentReceipt(messageId: message.id)
|> deliverOnMainQueue).start(next: { [weak self] receipt in
guard let self else {
return
}
self.push(self.context.sharedContext.makeStarsReceiptScreen(context: self.context, receipt: receipt))
})
} else {
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: message.id), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
/*for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
//strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId))
break
}
}*/
return true
case .setChatTheme:
strongSelf.presentThemeSelection()
return true
case let .setChatWallpaper(wallpaper, _):
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return true
}
if let peer = peer as? TelegramChannel {
if peer.flags.contains(.isCreator) || peer.adminRights?.rights.contains(.canChangeInfo) == true {
let _ = (context.engine.peers.getChannelBoostStatus(peerId: peer.id)
|> deliverOnMainQueue).start(next: { [weak self] boostStatus in
guard let self else {
return
}
self.push(ChannelAppearanceScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peerId: peer.id, boostStatus: boostStatus))
})
}
return true
}
guard message.effectivelyIncoming(strongSelf.context.account.peerId), let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
strongSelf.presentThemeSelection()
return true
}
strongSelf.chatDisplayNode.dismissInput()
var options = WallpaperPresentationOptions()
var intensity: Int32?
if let settings = wallpaper.settings {
if settings.blur {
options.insert(.blur)
}
if settings.motion {
options.insert(.motion)
}
if case let .file(file) = wallpaper, !file.isPattern {
intensity = settings.intensity
}
}
let wallpaperPreviewController = WallpaperGalleryController(context: strongSelf.context, source: .wallpaper(wallpaper, options, [], intensity, nil, nil), mode: .peer(EnginePeer(peer), true))
wallpaperPreviewController.apply = { [weak wallpaperPreviewController] entry, options, _, _, brightness, forBoth in
var settings: WallpaperSettings?
if case let .wallpaper(wallpaper, _) = entry {
let baseSettings = wallpaper.settings
var intensity: Int32? = baseSettings?.intensity
if case let .file(file) = wallpaper, !file.isPattern {
if let brightness {
intensity = max(0, min(100, Int32(brightness * 100.0)))
}
}
settings = WallpaperSettings(blur: options.contains(.blur), motion: options.contains(.motion), colors: baseSettings?.colors ?? [], intensity: intensity, rotation: baseSettings?.rotation)
}
let _ = (strongSelf.context.engine.themes.setExistingChatWallpaper(messageId: message.id, settings: settings, forBoth: forBoth)
|> deliverOnMainQueue).startStandalone()
Queue.mainQueue().after(0.1) {
wallpaperPreviewController?.dismiss()
}
}
strongSelf.push(wallpaperPreviewController)
return true
case let .giftPremium(_, _, duration, _, _):
strongSelf.chatDisplayNode.dismissInput()
let fromPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? strongSelf.context.account.peerId : message.id.peerId
let toPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? message.id.peerId : strongSelf.context.account.peerId
let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration, giftCode: nil))
strongSelf.push(controller)
return true
case let .giftCode(slug, _, _, _, _, _, _, _, _):
strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id, progress: params.progress)
return true
case let .suggestedProfilePhoto(image):
strongSelf.chatDisplayNode.dismissInput()
if let image = image {
if message.effectivelyIncoming(strongSelf.context.account.peerId) {
if let emojiMarkup = image.emojiMarkup {
let controller = AvatarEditorScreen(context: strongSelf.context, inputData: AvatarEditorScreen.inputData(context: strongSelf.context, isGroup: false), peerType: .user, markup: emojiMarkup)
controller.imageCompletion = { [weak self] image, commit in
if let strongSelf = self {
if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl {
settingsController.updateProfilePhoto(image, mode: .accept)
commit()
}
}
}
controller.videoCompletion = { [weak self] image, url, adjustments, commit in
if let strongSelf = self {
if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl {
settingsController.updateProfileVideo(image, mode: .accept, asset: AVURLAsset(url: url), adjustments: adjustments)
commit()
}
}
}
strongSelf.push(controller)
} else {
var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if let result = itemNode.transitionNode(id: message.id, media: image, adjustRect: false) {
selectedNode = result
}
}
}
let transitionView = selectedNode?.0.view
let senderName: String?
if let peer = message.peers[message.id.peerId] {
senderName = EnginePeer(peer).compactDisplayTitle
} else {
senderName = nil
}
legacyAvatarEditor(context: strongSelf.context, media: .message(message: MessageReference(message), media: image), transitionView: transitionView, senderName: senderName, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, imageCompletion: { [weak self] image in
if let strongSelf = self {
if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl {
settingsController.updateProfilePhoto(image, mode: .accept)
}
}
}, videoCompletion: { [weak self] image, url, adjustments in
if let strongSelf = self {
if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl {
settingsController.updateProfileVideo(image, mode: .accept, asset: AVURLAsset(url: url), adjustments: adjustments)
}
}
})
}
} else {
openMessageByAction = true
}
}
case .boostsApplied:
strongSelf.controllerInteraction?.openGroupBoostInfo(nil, 0)
return true
default:
break
}
if !openMessageByAction {
return true
}
}
}
let openChatLocation = strongSelf.chatLocation
var chatFilterTag: MemoryBuffer?
if case let .customTag(value, _) = strongSelf.chatDisplayNode.historyNode.tag {
chatFilterTag = value
}
var standalone = false
if case .customChatContents = strongSelf.chatLocation {
standalone = true
}
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, mediaIndex: params.mediaIndex, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
self?.chatDisplayNode.dismissInput()
}, present: { c, a in
self?.present(c, in: .window(.root), with: a, blockInteraction: true)
}, transitionNode: { messageId, media, adjustRect in
var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
if let strongSelf = self {
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if let result = itemNode.transitionNode(id: messageId, media: media, adjustRect: adjustRect) {
selectedNode = result
}
}
}
}
return selectedNode
}, addToTransitionSurface: { view in
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view)
}, openUrl: { url in
self?.openUrl(url, concealed: false, skipConcealedAlert: isLocation, message: nil)
}, openPeer: { peer, navigation in
self?.openPeer(peer: EnginePeer(peer), navigation: navigation, fromMessage: nil)
}, callPeer: { peerId, isVideo in
self?.controllerInteraction?.callPeer(peerId, isVideo)
}, enqueueMessage: { message in
self?.sendMessages([message])
}, sendSticker: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { fileReference, sourceNode, sourceRect in
return self?.controllerInteraction?.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) ?? false
} : nil, sendEmoji: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { text, attribute in
self?.controllerInteraction?.sendEmoji(text, attribute, false)
} : nil, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in
if let strongSelf = self {
strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).startStrict(next: { entry in
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
var messageIdAndMedia: [MessageId: [Media]] = [:]
if let entry = entry as? InstantPageGalleryEntry, entry.index == centralIndex {
messageIdAndMedia[message.id] = [galleryMedia]
}
controllerInteraction.hiddenMedia = messageIdAndMedia
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateHiddenMedia()
}
}
}
}))
}
}, chatAvatarHiddenMedia: { signal, media in
if let strongSelf = self {
strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).startStrict(next: { messageId in
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
var messageIdAndMedia: [MessageId: [Media]] = [:]
if let messageId = messageId {
messageIdAndMedia[messageId] = [media]
}
controllerInteraction.hiddenMedia = messageIdAndMedia
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateHiddenMedia()
}
}
}
}))
}
}, actionInteraction: GalleryControllerActionInteraction(
openUrl: { [weak self] url, concealed in
if let strongSelf = self {
strongSelf.openUrl(url, concealed: concealed, message: nil)
}
}, openUrlIn: { [weak self] url in
if let strongSelf = self {
strongSelf.openUrlIn(url)
}
}, openPeerMention: { [weak self] mention in
if let strongSelf = self {
strongSelf.controllerInteraction?.openPeerMention(mention, nil)
}
}, openPeer: { [weak self] peer in
if let strongSelf = self {
strongSelf.controllerInteraction?.openPeer(peer, .default, nil, .default)
}
}, openHashtag: { [weak self] peerName, hashtag in
if let strongSelf = self {
strongSelf.controllerInteraction?.openHashtag(peerName, hashtag)
}
}, openBotCommand: { [weak self] command in
if let strongSelf = self {
strongSelf.controllerInteraction?.sendBotCommand(nil, command)
}
}, addContact: { [weak self] phoneNumber in
if let strongSelf = self {
strongSelf.controllerInteraction?.addContact(phoneNumber)
}
}, storeMediaPlaybackState: { [weak self] messageId, timestamp, playbackRate in
guard let strongSelf = self else {
return
}
var storedState: MediaPlaybackStoredState?
if let timestamp = timestamp {
storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: AudioPlaybackRate(playbackRate))
}
let _ = updateMediaPlaybackStoredStateInteractively(engine: strongSelf.context.engine, messageId: messageId, state: storedState).startStandalone()
}, editMedia: { [weak self] messageId, snapshots, transitionCompletion in
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] message in
guard let strongSelf = self, let message = message else {
return
}
var mediaReference: AnyMediaReference?
for media in message.media {
if let image = media as? TelegramMediaImage {
mediaReference = AnyMediaReference.standalone(media: image)
} else if let file = media as? TelegramMediaFile {
mediaReference = AnyMediaReference.standalone(media: file)
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if let image = content.image {
mediaReference = AnyMediaReference.standalone(media: image)
} else if let file = content.file {
mediaReference = AnyMediaReference.standalone(media: file)
}
}
}
if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] {
legacyMediaEditor(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, media: mediaReference, mode: .draw, initialCaption: NSAttributedString(), snapshots: snapshots, transitionCompletion: {
transitionCompletion()
}, getCaptionPanelView: { [weak self] in
return self?.getCaptionPanelView(isFile: false)
}, sendMessagesWithSignals: { [weak self] signals, _, _ in
if let strongSelf = self {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: false)
}
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
})
}
})
}, updateCanReadHistory: { [weak self] canReadHistory in
self?.canReadHistory.set(canReadHistory)
}),
getSourceRect: { [weak self] in
guard let strongSelf = self else {
return nil
}
var rect: CGRect?
strongSelf.chatDisplayNode.historyNode.forEachVisibleMessageItemNode({ itemNode in
if itemNode.item?.message.id == message.id {
rect = itemNode.view.convert(itemNode.contentFrame(), to: nil)
}
})
return rect
}
))
}, openPeer: { [weak self] peer, navigation, fromMessage, source in
var expandAvatar = false
if case let .groupParticipant(storyStats, avatarHeaderNode) = source {
if let storyStats, storyStats.totalCount != 0, let avatarHeaderNode = avatarHeaderNode as? ChatMessageAvatarHeaderNodeImpl {
self?.openStories(peerId: peer.id, avatarHeaderNode: avatarHeaderNode, avatarNode: nil)
return
} else {
expandAvatar = true
}
}
var fromReactionMessageId: MessageId?
if case .reaction = source {
fromReactionMessageId = fromMessage?.id
}
self?.openPeer(peer: peer, navigation: navigation, fromMessage: fromMessage, fromReactionMessageId: fromReactionMessageId, expandAvatar: expandAvatar)
}, openPeerMention: { [weak self] name, progress in
self?.openPeerMention(name, progress: progress)
}, openMessageContextMenu: { [weak self] message, selectAll, node, frame, anyRecognizer, location in
guard let self, self.isNodeLoaded else {
return
}
self.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame, anyRecognizer: anyRecognizer, location: location)
}, openMessageReactionContextMenu: { [weak self] message, sourceView, gesture, value in
guard let self else {
return
}
self.openMessageReactionContextMenu(message: message, sourceView: sourceView, gesture: gesture, value: value)
}, updateMessageReaction: { [weak self] initialMessage, reaction, force, sourceView in
guard let strongSelf = self else {
return
}
guard let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(initialMessage.id) else {
return
}
guard let message = messages.first else {
return
}
if case .default = reaction, strongSelf.chatLocation.peerId == strongSelf.context.account.peerId {
return
}
if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject {
if case let .hashTagSearch(publicPosts) = customChatContents.kind, publicPosts {
return
}
}
if !force && message.areReactionsTags(accountPeerId: strongSelf.context.account.peerId) {
if case .pinnedMessages = strongSelf.subject {
return
}
if !strongSelf.presentationInterfaceState.isPremium {
strongSelf.presentTagPremiumPaywall()
return
}
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else {
return
}
guard item.message.id == message.id else {
return
}
let chosenReaction: MessageReaction.Reaction?
switch reaction {
case .default:
switch item.associatedData.defaultReaction {
case .none:
chosenReaction = nil
case let .builtin(value):
chosenReaction = .builtin(value)
case let .custom(fileId):
chosenReaction = .custom(fileId)
}
case let .reaction(value):
switch value {
case let .builtin(value):
chosenReaction = .builtin(value)
case let .custom(fileId):
chosenReaction = .custom(fileId)
}
}
guard let chosenReaction = chosenReaction else {
return
}
let tag = ReactionsMessageAttribute.messageTag(reaction: chosenReaction)
if strongSelf.presentationInterfaceState.historyFilter?.customTag == tag {
if let sourceView {
strongSelf.openMessageReactionContextMenu(message: message, sourceView: sourceView, gesture: nil, value: chosenReaction)
}
} else {
strongSelf.chatDisplayNode.historyNode.frozenMessageForScrollingReset = message.id
strongSelf.interfaceInteraction?.updateHistoryFilter { _ in
return ChatPresentationInterfaceState.HistoryFilter(customTag: tag, isActive: true)
}
}
}
return
}
let _ = (peerMessageAllowedReactions(context: strongSelf.context, message: message)
|> deliverOnMainQueue).startStandalone(next: { allowedReactions in
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else {
return
}
guard item.message.id == message.id else {
return
}
let chosenReaction: MessageReaction.Reaction?
switch reaction {
case .default:
switch item.associatedData.defaultReaction {
case .none:
chosenReaction = nil
case let .builtin(value):
chosenReaction = .builtin(value)
case let .custom(fileId):
chosenReaction = .custom(fileId)
}
case let .reaction(value):
switch value {
case let .builtin(value):
chosenReaction = .builtin(value)
case let .custom(fileId):
chosenReaction = .custom(fileId)
}
}
guard let chosenReaction = chosenReaction else {
return
}
var removedReaction: MessageReaction.Reaction?
var messageAlreadyHasThisReaction = false
let currentReactions = mergedMessageReactions(attributes: message.attributes, isTags: message.areReactionsTags(accountPeerId: context.account.peerId))?.reactions ?? []
var updatedReactions: [MessageReaction.Reaction] = currentReactions.filter(\.isSelected).map(\.value)
if let index = updatedReactions.firstIndex(where: { $0 == chosenReaction }) {
removedReaction = chosenReaction
updatedReactions.remove(at: index)
} else {
updatedReactions.append(chosenReaction)
messageAlreadyHasThisReaction = currentReactions.contains(where: { $0.value == chosenReaction })
}
if removedReaction == nil {
if !canAddMessageReactions(message: message) {
itemNode.openMessageContextMenu()
return
}
if strongSelf.context.sharedContext.immediateExperimentalUISettings.disableQuickReaction {
itemNode.openMessageContextMenu()
return
}
guard let allowedReactions = allowedReactions else {
itemNode.openMessageContextMenu()
return
}
switch allowedReactions {
case let .set(set):
if !messageAlreadyHasThisReaction && updatedReactions.contains(where: { !set.contains($0) }) {
itemNode.openMessageContextMenu()
return
}
case .all:
break
}
}
if removedReaction == nil && !updatedReactions.isEmpty {
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.tap()
itemNode.awaitingAppliedReaction = (chosenReaction, { [weak itemNode] in
guard let strongSelf = self else {
return
}
if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: chosenReaction) {
var reactionItem: ReactionItem?
switch chosenReaction {
case .builtin:
for reaction in availableReactions.reactions {
guard let centerAnimation = reaction.centerAnimation else {
continue
}
guard let aroundAnimation = reaction.aroundAnimation else {
continue
}
if reaction.value == chosenReaction {
reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: reaction.value),
appearAnimation: reaction.appearAnimation,
stillAnimation: reaction.selectAnimation,
listAnimation: centerAnimation,
largeListAnimation: reaction.activateAnimation,
applicationAnimation: aroundAnimation,
largeApplicationAnimation: reaction.effectAnimation,
isCustom: false
)
break
}
}
case let .custom(fileId):
if let itemFile = item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile {
reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: chosenReaction),
appearAnimation: itemFile,
stillAnimation: itemFile,
listAnimation: itemFile,
largeListAnimation: itemFile,
applicationAnimation: nil,
largeApplicationAnimation: nil,
isCustom: true
)
}
}
if let reactionItem = reactionItem {
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: strongSelf.chatDisplayNode.historyNode.takeGenericReactionEffect())
strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)
standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds
standaloneReactionAnimation.animateReactionSelection(
context: strongSelf.context,
theme: strongSelf.presentationData.theme,
animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache,
reaction: reactionItem,
avatarPeers: [],
playHaptic: false,
isLarge: false,
targetView: targetView,
addStandaloneReactionAnimation: { standaloneReactionAnimation in
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds
strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)
},
completion: { [weak standaloneReactionAnimation] in
standaloneReactionAnimation?.removeFromSupernode()
}
)
}
}
})
} else {
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts(itemNode: itemNode)
if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message, isPremium: strongSelf.presentationInterfaceState.isPremium, forceInline: false) {
var hideRemovedReaction: Bool = false
if let reactions = mergedMessageReactions(attributes: message.attributes, isTags: message.areReactionsTags(accountPeerId: context.account.peerId)) {
for reaction in reactions.reactions {
if reaction.value == removedReaction {
hideRemovedReaction = reaction.count == 1
break
}
}
}
let standaloneDismissAnimation = StandaloneDismissReactionAnimation()
standaloneDismissAnimation.frame = strongSelf.chatDisplayNode.bounds
strongSelf.chatDisplayNode.addSubnode(standaloneDismissAnimation)
standaloneDismissAnimation.animateReactionDismiss(sourceView: targetView, hideNode: hideRemovedReaction, isIncoming: message.effectivelyIncoming(strongSelf.context.account.peerId), completion: { [weak standaloneDismissAnimation] in
standaloneDismissAnimation?.removeFromSupernode()
})
}
}
let mappedUpdatedReactions = updatedReactions.map { reaction -> UpdateMessageReaction in
switch reaction {
case let .builtin(value):
return .builtin(value)
case let .custom(fileId):
return .custom(fileId: fileId, file: nil)
}
}
if !strongSelf.presentationInterfaceState.isPremium && mappedUpdatedReactions.count > strongSelf.context.userLimits.maxReactionsPerMessage {
let _ = (ApplicationSpecificNotice.incrementMultipleReactionsSuggestion(accountManager: strongSelf.context.sharedContext.accountManager)
|> deliverOnMainQueue).startStandalone(next: { [weak self] count in
guard let self else {
return
}
if count < 1 {
let context = self.context
let controller = UndoOverlayController(
presentationData: self.presentationData,
content: .premiumPaywall(title: nil, text: self.presentationData.strings.Chat_Reactions_MultiplePremiumTooltip, customUndoText: nil, timeout: nil, linkAction: nil),
elevatedLayout: false,
action: { [weak self] action in
if case .info = action {
if let self {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .reactions, forceDark: false, dismissed: nil)
self.push(controller)
}
}
return true
}
)
self.present(controller, in: .current)
}
})
}
let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageIds: [message.id], reactions: mappedUpdatedReactions, isLarge: false, storeAsRecentlyUsed: false).startStandalone()
#if DEBUG
if strongSelf.context.sharedContext.applicationBindings.appBuildType == .internal {
if mappedUpdatedReactions.contains(where: {
if case let .custom(fileId, _) = $0, fileId == MessageReaction.starsReactionId {
return true
} else {
return false
}
}) {
let _ = (strongSelf.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId])
|> deliverOnMainQueue).start(next: { [weak strongSelf] files in
guard let strongSelf, let file = files[MessageReaction.starsReactionId] else {
return
}
//TODO:localize
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .starsSent(context: strongSelf.context, file: file, amount: 1, title: "Star Sent", text: "Long tap on {star} to select custom quantity of stars."), elevatedLayout: false, action: { _ in
return false
}), in: .current)
})
}
}
#endif
}
})
}, activateMessagePinch: { [weak self] sourceNode in
guard let strongSelf = self else {
return
}
var sourceItemNode: ListViewItemNode?
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
guard let itemNode = itemNode as? ListViewItemNode else {
return
}
if sourceNode.view.isDescendant(of: itemNode.view) {
sourceItemNode = itemNode
}
}
let isSecret = strongSelf.presentationInterfaceState.copyProtectionEnabled || strongSelf.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat
let pinchController = PinchController(sourceNode: sourceNode, disableScreenshots: isSecret, getContentAreaInScreenSpace: {
guard let strongSelf = self else {
return CGRect()
}
return strongSelf.chatDisplayNode.view.convert(strongSelf.chatDisplayNode.frameForVisibleArea(), to: nil)
})
strongSelf.currentPinchController = pinchController
strongSelf.currentPinchSourceItemNode = sourceItemNode
strongSelf.window?.presentInGlobalOverlay(pinchController)
}, openMessageContextActions: { message, node, rect, gesture in
gesture?.cancel()
}, navigateToMessage: { [weak self] fromId, id, params in
guard let self else {
return
}
self.navigateToMessage(fromId: fromId, id: id, params: params)
}, navigateToMessageStandalone: { [weak self] id in
self?.navigateToMessage(from: nil, to: .id(id, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: false)
}, navigateToThreadMessage: { [weak self] peerId, threadId, messageId in
if let context = self?.context, let navigationController = self?.effectiveNavigationController {
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always).startStandalone()
}
}, tapMessage: nil, clickThroughMessage: { [weak self] in
self?.chatDisplayNode.dismissInput()
}, toggleMessagesSelection: { [weak self] ids, value in
guard let strongSelf = self, strongSelf.isNodeLoaded else {
return
}
if let subject = strongSelf.subject, case .messageOptions = subject, !value {
let selectedCount = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds.count ?? 0
let updatedSelectedCount = selectedCount - ids.count
if updatedSelectedCount < 1 {
return
}
}
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessages(ids, value: value) } })
if let selectionState = strongSelf.presentationInterfaceState.interfaceState.selectionState {
let count = selectionState.selectedIds.count
let text = strongSelf.presentationData.strings.VoiceOver_Chat_MessagesSelected(Int32(count))
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: text as NSString)
})
}
}, sendCurrentMessage: { [weak self] silentPosting, messageEffect in
if let strongSelf = self {
if let _ = strongSelf.presentationInterfaceState.interfaceState.mediaDraftState {
strongSelf.sendMediaRecording(silentPosting: silentPosting, messageEffect: messageEffect)
} else {
strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, messageEffect: messageEffect)
}
}
}, sendMessage: { [weak self] text in
guard let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) else {
return
}
var isScheduledMessages = false
if case .scheduledMessages = strongSelf.presentationInterfaceState.subject {
isScheduledMessages = true
}
guard !isScheduledMessages else {
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }
})
}
}, nil)
var attributes: [MessageAttribute] = []
let entities = generateTextEntities(text, enabledTypes: .all)
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
let peerId = strongSelf.chatLocation.peerId
if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = strongSelf.chatDisplayNode.interactiveEmojis, interactiveEmojis.emojis.contains(text) {
strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: text)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
} else {
strongSelf.sendMessages([.message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
}
}, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets in
guard let strongSelf = self else {
return false
}
if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceView, sourceRect)
return false
}
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, peer.hasBannedPermission(.banSendStickers) != nil {
if let boostsToUnrestrict = strongSelf.presentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0, (strongSelf.presentationInterfaceState.appliedBoosts ?? 0) < boostsToUnrestrict {
strongSelf.interfaceInteraction?.openBoostToUnrestrict()
return false
}
}
var attributes: [MessageAttribute] = []
if let query = query {
attributes.append(EmojiSearchQueryMessageAttribute(query: query))
}
let correlationId = Int64.random(in: 0 ..< Int64.max)
var replyPanel: ReplyAccessoryPanelNode?
if let accessoryPanelNode = strongSelf.chatDisplayNode.accessoryPanelNode as? ReplyAccessoryPanelNode {
replyPanel = accessoryPanelNode
}
var shouldAnimateMessageTransition = strongSelf.chatDisplayNode.shouldAnimateMessageTransition
if let _ = sourceView.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode {
shouldAnimateMessageTransition = true
}
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in
var current = current
current = current.updatedInterfaceState { interfaceState in
var interfaceState = interfaceState
interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil)
if clearInput {
interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString()))
}
return interfaceState
}.updatedInputMode { current in
if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil {
return .media(mode: mode, expanded: nil, focused: focused)
}
return current
}
return current
})
}
}, shouldAnimateMessageTransition ? correlationId : nil)
if shouldAnimateMessageTransition {
if let sourceNode = sourceView.asyncdisplaykit_node as? ChatMediaInputStickerGridItemNode {
strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: {
guard let strongSelf = self else {
return
}
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in
var current = current
current = current.updatedInputMode { current in
if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil {
return .media(mode: mode, expanded: nil, focused: focused)
}
return current
}
return current
})
})
} else if let sourceNode = sourceView.asyncdisplaykit_node as? HorizontalStickerGridItemNode {
strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .mediaPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: {})
} else if let sourceNode = sourceView.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode {
strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .emptyPanel(itemNode: sourceNode), replyPanel: nil), initiated: {})
} else if let sourceLayer = sourceLayer {
strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .universal(sourceContainerView: sourceView, sourceRect: sourceRect, sourceLayer: sourceLayer), replyPanel: replyPanel), initiated: {
guard let strongSelf = self else {
return
}
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in
var current = current
current = current.updatedInputMode { current in
if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil {
return .media(mode: mode, expanded: nil, focused: focused)
}
return current
}
return current
})
})
}
}
let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)]
if silentPosting {
let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting)
strongSelf.sendMessages(transformedMessages)
} else if schedule {
strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in
if let strongSelf = self {
let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime)
strongSelf.sendMessages(transformedMessages)
}
})
} else {
let transformedMessages = strongSelf.transformEnqueueMessages(messages)
strongSelf.sendMessages(transformedMessages)
}
return true
}, sendEmoji: { [weak self] text, attribute, immediately in
if let strongSelf = self {
if immediately {
if let file = attribute.file {
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
for attribute in file.attributes {
if case let .CustomEmoji(_, _, _, packReference) = attribute {
if case let .id(id, _) = packReference {
bubbleUpEmojiOrStickersets.append(ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id))
}
}
}
strongSelf.sendMessages([.message(text: text, attributes: [TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id))])], inlineStickers: [file.fileId : file], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)], commit: false)
}
} else {
strongSelf.interfaceInteraction?.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: attribute]))
strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in
return state.updatedInputMode({ _ in
return .text
})
})
let _ = (ApplicationSpecificNotice.getEmojiTooltip(accountManager: strongSelf.context.sharedContext.accountManager)
|> deliverOnMainQueue).startStandalone(next: { count in
guard let strongSelf = self else {
return
}
if count < 2 {
let _ = ApplicationSpecificNotice.incrementEmojiTooltip(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone()
Queue.mainQueue().after(0.5, {
strongSelf.displayEmojiTooltip()
})
}
})
}
}
}, sendGif: { [weak self] fileReference, sourceView, sourceRect, silentPosting, schedule in
if let strongSelf = self {
if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceView, sourceRect)
return false
}
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, peer.hasBannedPermission(.banSendGifs) != nil {
if let boostsToUnrestrict = strongSelf.presentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0, (strongSelf.presentationInterfaceState.appliedBoosts ?? 0) < boostsToUnrestrict {
strongSelf.interfaceInteraction?.openBoostToUnrestrict()
return false
}
}
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }.updatedInputMode { current in
if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil {
return .media(mode: mode, expanded: nil, focused: focused)
}
return current
}
})
}
}, nil)
var messages = [EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]
if silentPosting {
messages = strongSelf.transformEnqueueMessages(messages, silentPosting: true)
strongSelf.sendMessages(messages)
} else if schedule {
strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in
if let strongSelf = self {
let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime)
strongSelf.sendMessages(transformedMessages)
}
})
} else {
messages = strongSelf.transformEnqueueMessages(messages)
strongSelf.sendMessages(messages)
}
}
return true
}, sendBotContextResultAsGif: { [weak self] collection, result, sourceView, sourceRect, silentPosting, resetTextInputState in
guard let strongSelf = self else {
return false
}
if case .pinnedMessages = strongSelf.presentationInterfaceState.subject {
return false
}
if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceView, sourceRect)
return false
}
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, peer.hasBannedPermission(.banSendGifs) != nil {
if let boostsToUnrestrict = strongSelf.presentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0, (strongSelf.presentationInterfaceState.appliedBoosts ?? 0) < boostsToUnrestrict {
strongSelf.interfaceInteraction?.openBoostToUnrestrict()
return false
}
}
strongSelf.enqueueChatContextResult(collection, result, hideVia: true, closeMediaInput: true, silentPosting: silentPosting, resetTextInputState: resetTextInputState)
return true
}, requestMessageActionCallback: { [weak self] messageId, data, isGame, requiresPassword in
guard let strongSelf = self else {
return
}
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> deliverOnMainQueue).startStandalone(next: { message in
guard let strongSelf = self, let message = message else {
return
}
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedTitlePanelContext {
if !$0.contains(where: {
switch $0 {
case .requestInProgress:
return true
default:
return false
}
}) {
var updatedContexts = $0
updatedContexts.append(.requestInProgress)
return updatedContexts.sorted()
}
return $0
}
})
let proceedWithResult: (MessageActionCallbackResult) -> Void = { [weak self] result in
guard let strongSelf = self else {
return
}
switch result {
case .none:
break
case let .alert(text):
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
case let .toast(text):
let message: Signal<String?, NoError> = .single(text)
let noMessage: Signal<String?, NoError> = .single(nil)
let delayedNoMessage: Signal<String?, NoError> = noMessage |> delay(1.0, queue: Queue.mainQueue())
strongSelf.botCallbackAlertMessage.set(message |> then(delayedNoMessage))
case let .url(url):
if isGame {
let openBot: () -> Void = {
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.dismissInput()
strongSelf.effectiveNavigationController?.pushViewController(GameController(context: strongSelf.context, url: url, message: message))
}
var botPeer: TelegramUser?
for attribute in message.attributes {
if let attribute = attribute as? InlineBotMessageAttribute {
if let peerId = attribute.peerId {
botPeer = message.peers[peerId] as? TelegramUser
}
}
}
if botPeer == nil {
if case let .user(peer) = message.author, peer.botInfo != nil {
botPeer = peer
} else if let peer = message.peers[message.id.peerId] as? TelegramUser, peer.botInfo != nil {
botPeer = peer
}
}
if let botPeer = botPeer {
let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id)
|> deliverOnMainQueue).startStandalone(next: { value in
guard let strongSelf = self else {
return
}
if value {
openBot()
} else {
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Conversation_BotInteractiveUrlAlert(EnginePeer(botPeer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
if let strongSelf = self {
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
openBot()
}
})]), in: .window(.root), with: nil)
}
})
}
} else {
strongSelf.openUrl(url, concealed: false)
}
}
}
let updateProgress = { [weak self] in
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedTitlePanelContext {
if let index = $0.firstIndex(where: {
switch $0 {
case .requestInProgress:
return true
default:
return false
}
}) {
var updatedContexts = $0
updatedContexts.remove(at: index)
return updatedContexts
}
return $0
}
})
}
}
}
let context = strongSelf.context
if requiresPassword {
strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestMessageActionCallbackPasswordCheck(messageId: messageId, isGame: isGame, data: data)
|> afterDisposed {
updateProgress()
})
|> deliverOnMainQueue).startStrict(error: { error in
let controller = ownershipTransferController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, initialError: error, present: { c, a in
strongSelf.present(c, in: .window(.root), with: a)
}, commit: { password in
return context.engine.messages.requestMessageActionCallback(messageId: messageId, isGame: isGame, password: password, data: data)
|> afterDisposed {
updateProgress()
}
}, completion: { result in
proceedWithResult(result)
})
strongSelf.present(controller, in: .window(.root))
}))
} else {
strongSelf.messageActionCallbackDisposable.set(((context.engine.messages.requestMessageActionCallback(messageId: messageId, isGame: isGame, password: nil, data: data)
|> afterDisposed {
updateProgress()
})
|> deliverOnMainQueue).startStrict(next: { result in
proceedWithResult(result)
}))
}
})
}, requestMessageActionUrlAuth: { [weak self] defaultUrl, subject in
if let strongSelf = self {
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedTitlePanelContext {
if !$0.contains(where: {
switch $0 {
case .requestInProgress:
return true
default:
return false
}
}) {
var updatedContexts = $0
updatedContexts.append(.requestInProgress)
return updatedContexts.sorted()
}
return $0
}
})
strongSelf.messageActionUrlAuthDisposable.set(((combineLatest(strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId), strongSelf.context.engine.messages.requestMessageActionUrlAuth(subject: subject) |> afterDisposed {
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedTitlePanelContext {
if let index = $0.firstIndex(where: {
switch $0 {
case .requestInProgress:
return true
default:
return false
}
}) {
var updatedContexts = $0
updatedContexts.remove(at: index)
return updatedContexts
}
return $0
}
})
}
}
})) |> deliverOnMainQueue).startStrict(next: { peer, result in
if let strongSelf = self {
switch result {
case .default:
strongSelf.openUrl(defaultUrl, concealed: false, skipUrlAuth: true)
case let .request(domain, bot, requestWriteAccess):
let controller = chatMessageActionUrlAuthController(context: strongSelf.context, defaultUrl: defaultUrl, domain: domain, bot: bot, requestWriteAccess: requestWriteAccess, displayName: EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), open: { [weak self] authorize, allowWriteAccess in
if let strongSelf = self {
if authorize {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedTitlePanelContext {
if !$0.contains(where: {
switch $0 {
case .requestInProgress:
return true
default:
return false
}
}) {
var updatedContexts = $0
updatedContexts.append(.requestInProgress)
return updatedContexts.sorted()
}
return $0
}
})
strongSelf.messageActionUrlAuthDisposable.set(((strongSelf.context.engine.messages.acceptMessageActionUrlAuth(subject: subject, allowWriteAccess: allowWriteAccess) |> afterDisposed {
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedTitlePanelContext {
if let index = $0.firstIndex(where: {
switch $0 {
case .requestInProgress:
return true
default:
return false
}
}) {
var updatedContexts = $0
updatedContexts.remove(at: index)
return updatedContexts
}
return $0
}
})
}
}
}) |> deliverOnMainQueue).startStrict(next: { [weak self] result in
if let strongSelf = self {
switch result {
case let .accepted(url):
strongSelf.openUrl(url, concealed: false, skipUrlAuth: true)
default:
strongSelf.openUrl(defaultUrl, concealed: false, skipUrlAuth: true)
}
}
}))
} else {
strongSelf.openUrl(defaultUrl, concealed: false, skipUrlAuth: true)
}
}
})
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(controller, in: .window(.root))
case let .accepted(url):
strongSelf.openUrl(url, concealed: false, forceExternal: true, skipUrlAuth: true)
}
}
}))
}
}, activateSwitchInline: { [weak self] peerId, inputString, peerTypes in
guard let strongSelf = self else {
return
}
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
if let botStart = strongSelf.botStart, case let .automatic(returnToPeerId, scheduled) = botStart.behavior {
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: returnToPeerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
if let strongSelf = self, let peer = peer {
strongSelf.openPeer(peer: peer, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), subject: scheduled ? .scheduledMessages : nil, peekData: nil), fromMessage: nil)
}
})
} else {
if let peerId = peerId {
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
if let strongSelf = self, let peer = peer {
strongSelf.openPeer(peer: peer, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), subject: nil, peekData: nil), fromMessage: nil)
}
})
} else {
strongSelf.openPeer(peer: nil, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), subject: nil, peekData: nil), fromMessage: nil, peerTypes: peerTypes)
}
}
}, openUrl: { [weak self] urlData in
guard let strongSelf = self else {
return
}
let url = urlData.url
let concealed = urlData.concealed
let message = urlData.message
let progress = urlData.progress
let forceExternal = urlData.external ?? false
var skipConcealedAlert = false
if let author = message?.author, author.isVerified {
skipConcealedAlert = true
}
if let message, let adAttribute = message.attributes.first(where: { $0 is AdMessageAttribute }) as? AdMessageAttribute {
strongSelf.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId)
}
if let performOpenURL = strongSelf.performOpenURL {
performOpenURL(message, url, progress)
} else {
strongSelf.openUrl(url, concealed: concealed, forceExternal: forceExternal, skipConcealedAlert: skipConcealedAlert, message: message, allowInlineWebpageResolution: urlData.allowInlineWebpageResolution, progress: progress)
}
}, shareCurrentLocation: { [weak self] in
if let strongSelf = self {
if case .pinnedMessages = strongSelf.presentationInterfaceState.subject {
return
}
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Conversation_ShareBotLocationConfirmationTitle, text: strongSelf.presentationData.strings.Conversation_ShareBotLocationConfirmation, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
if let strongSelf = self, let locationManager = strongSelf.context.sharedContext.locationManager {
let _ = (currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0)
|> deliverOnMainQueue).startStandalone(next: { coordinate in
if let strongSelf = self {
if let coordinate = coordinate {
strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
} else {
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})]), in: .window(.root))
}
}
})
}
})]), in: .window(.root))
}
}, shareAccountContact: { [weak self] in
if let strongSelf = self {
if case .pinnedMessages = strongSelf.presentationInterfaceState.subject {
return
}
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Conversation_ShareBotContactConfirmationTitle, text: strongSelf.presentationData.strings.Conversation_ShareBotContactConfirmation, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
if let strongSelf = self {
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId)
|> deliverOnMainQueue).startStandalone(next: { peer in
if let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty {
strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
}
})
}
})]), in: .window(.root))
}
}, sendBotCommand: { [weak self] messageId, command in
if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) {
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}, nil)
var postAsReply = false
if !command.contains("@") {
switch strongSelf.chatLocation {
case let .peer(peerId):
if (peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup) {
postAsReply = true
}
case .replyThread:
postAsReply = true
case .customChatContents:
postAsReply = true
}
if let messageId = messageId, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
if let author = message.author as? TelegramUser, author.botInfo != nil {
} else {
postAsReply = false
}
}
}
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) }
})
}
}, nil)
var attributes: [MessageAttribute] = []
let entities = generateTextEntities(command, enabledTypes: .all)
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
var replyToMessageId: EngineMessageReplySubject?
if postAsReply, let messageId {
replyToMessageId = EngineMessageReplySubject(messageId: messageId, quote: nil)
}
strongSelf.sendMessages([.message(text: command, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
}
}, openInstantPage: { [weak self] message, associatedData in
if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.effectiveNavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) {
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
strongSelf.chatDisplayNode.dismissInput()
strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController)
if case .overlay = strongSelf.presentationInterfaceState.mode {
strongSelf.chatDisplayNode.dismissAsOverlay()
}
})
}
}, openWallpaper: { [weak self] message in
if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) {
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
strongSelf.chatDisplayNode.dismissInput()
strongSelf.context.sharedContext.openChatWallpaper(context: strongSelf.context, message: message, present: { [weak self] c, a in
self?.push(c)
})
})
}
}, openTheme: { [weak self] message in
if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) {
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
strongSelf.chatDisplayNode.dismissInput()
openChatTheme(context: strongSelf.context, message: message, pushController: { [weak self] c in
self?.effectiveNavigationController?.pushViewController(c)
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a, blockInteraction: true)
})
})
}
}, openHashtag: { [weak self] peerName, hashtag in
guard let strongSelf = self else {
return
}
strongSelf.openHashtag(hashtag, peerName: peerName)
}, updateInputState: { [weak self] f in
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedInterfaceState {
let updatedState: ChatTextInputState
if canSendMessagesToChat(strongSelf.presentationInterfaceState) {
updatedState = f($0.effectiveInputState)
} else {
updatedState = ChatTextInputState()
}
return $0.withUpdatedEffectiveInputState(updatedState)
}
})
}
}, updateInputMode: { [weak self] f in
self?.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedInputMode(f)
})
}, openMessageShareMenu: { [weak self] id in
guard let self else {
return
}
self.openMessageShareMenu(id: id)
}, presentController: { [weak self] controller, arguments in
self?.present(controller, in: .window(.root), with: arguments)
}, presentControllerInCurrent: { [weak self] controller, arguments in
if controller is UndoOverlayController {
self?.dismissAllTooltips()
}
self?.present(controller, in: .current, with: arguments)
}, navigationController: { [weak self] in
return self?.navigationController as? NavigationController
}, chatControllerNode: { [weak self] in
return self?.chatDisplayNode
}, presentGlobalOverlayController: { [weak self] controller, arguments in
self?.presentInGlobalOverlay(controller, with: arguments)
}, callPeer: { [weak self] peerId, isVideo in
if let strongSelf = self {
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
strongSelf.commitPurposefulAction()
let _ = (context.account.viewTracker.peerView(peerId)
|> take(1)
|> map { view -> Peer? in
return peerViewMainPeer(view)
}
|> deliverOnMainQueue).startStandalone(next: { peer in
guard let peer = peer else {
return
}
if let cachedUserData = strongSelf.peerView?.cachedData as? CachedUserData, cachedUserData.callsPrivate {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: presentationData.strings.Call_ConnectionErrorTitle, text: presentationData.strings.Call_PrivacyErrorMessage(EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
context.requestCall(peerId: peer.id, isVideo: isVideo, completion: {})
})
})
}
}, longTap: { [weak self] action, params in
if let self {
self.openLinkLongTap(action, params: params)
}
}, openCheckoutOrReceipt: { [weak self] messageId, params in
guard let strongSelf = self else {
return
}
strongSelf.commitPurposefulAction()
var isScheduledMessages = false
if case .scheduledMessages = strongSelf.presentationInterfaceState.subject {
isScheduledMessages = true
}
guard !isScheduledMessages else {
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> deliverOnMainQueue).startStandalone(next: { message in
guard let strongSelf = self, let message else {
return
}
for media in message.media {
if let paidContent = media as? TelegramMediaPaidContent {
let progressSignal = Signal<Never, NoError> { _ in
params?.progress?.set(.single(true))
return ActionDisposable {
params?.progress?.set(.single(false))
}
}
|> runOn(Queue.mainQueue())
|> delay(0.25, queue: Queue.mainQueue())
let progressDisposable = progressSignal.startStrict()
strongSelf.chatDisplayNode.dismissInput()
let inputData = Promise<BotCheckoutController.InputData?>()
inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .message(message.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
return .single(nil)
})
if let starsContext = strongSelf.context.starsContext {
let starsInputData = combineLatest(
inputData.get(),
starsContext.state
)
|> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in
if let data, let state {
return (state, data.form, data.botPeer)
} else {
return nil
}
}
let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
guard let strongSelf = self, let extendedMedia = paidContent.extendedMedia.first, case let .preview(dimensions, immediateThumbnailData, _) = extendedMedia else {
return
}
var messageId = messageId
if let sourceMessageId = message.forwardInfo?.sourceMessageId {
messageId = sourceMessageId
}
let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: paidContent.amount, startParam: "", extendedMedia: .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: nil), flags: [], version: 0)
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), extendedMedia: paidContent.extendedMedia, inputData: starsInputData, completion: { _ in })
strongSelf.push(controller)
progressDisposable.dispose()
})
}
} else if let invoice = media as? TelegramMediaInvoice {
strongSelf.chatDisplayNode.dismissInput()
if let receiptMessageId = invoice.receiptMessageId {
if invoice.currency == "XTR" {
let _ = (strongSelf.context.engine.payments.requestBotPaymentReceipt(messageId: receiptMessageId)
|> deliverOnMainQueue).start(next: { [weak self] receipt in
guard let strongSelf = self else {
return
}
strongSelf.push(strongSelf.context.sharedContext.makeStarsReceiptScreen(context: strongSelf.context, receipt: receipt))
})
} else {
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
} else {
let inputData = Promise<BotCheckoutController.InputData?>()
inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .message(message.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
return .single(nil)
})
if invoice.currency == "XTR", let starsContext = strongSelf.context.starsContext {
let starsInputData = combineLatest(
inputData.get(),
starsContext.state
)
|> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in
if let data, let state {
return (state, data.form, data.botPeer)
} else {
return nil
}
}
let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
guard let strongSelf = self else {
return
}
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), extendedMedia: [], inputData: starsInputData, completion: { _ in })
strongSelf.push(controller)
})
} else {
strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .message(messageId), inputData: inputData, completed: { currencyValue, receiptMessageId in
guard let strongSelf = self else {
return
}
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in
guard let strongSelf = self, let receiptMessageId = receiptMessageId else {
return false
}
if case .info = action {
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
return true
}
return false
}), in: .current)
}), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
}
}
}
})
}, openSearch: {
}, setupReply: { [weak self] messageId in
self?.interfaceInteraction?.setupReplyMessage(messageId, { _, f in f() })
}, canSetupReply: { [weak self] message in
if message.adAttribute != nil {
return .none
}
if !message.flags.contains(.Incoming) {
if !message.flags.intersection([.Failed, .Sending, .Unsent]).isEmpty {
return .none
}
}
if let strongSelf = self {
if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation, replyThreadMessage.effectiveMessageId == message.id {
return .none
}
if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation, replyThreadMessage.peerId == strongSelf.context.account.peerId {
if replyThreadMessage.threadId != strongSelf.context.account.peerId.toInt64() {
return .none
}
}
if case .peer = strongSelf.chatLocation, let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) {
if message.threadId == nil {
return .none
}
}
if canReplyInChat(strongSelf.presentationInterfaceState, accountPeerId: strongSelf.context.account.peerId) {
return .reply
} else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
}
}
return .none
}, canSendMessages: { [weak self] in
guard let self else {
return false
}
return canSendMessagesToChat(self.presentationInterfaceState)
}, navigateToFirstDateMessage: { [weak self] timestamp, alreadyThere in
guard let strongSelf = self else {
return
}
switch strongSelf.chatLocation {
case let .peer(peerId):
if alreadyThere {
strongSelf.openCalendarSearch(timestamp: timestamp)
} else {
strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, animated: true, completion: nil)
}
case let .replyThread(replyThreadMessage):
let peerId = replyThreadMessage.peerId
strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, forceInCurrentChat: true, animated: true, completion: nil)
case .customChatContents:
break
}
}, requestRedeliveryOfFailedMessages: { [weak self] id in
guard let strongSelf = self else {
return
}
if id.namespace == Namespaces.Message.ScheduledCloud {
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.MessageGroup(id: id))
|> deliverOnMainQueue).startStandalone(next: { messages in
guard let strongSelf = self, let message = messages.filter({ $0.id == id }).first else {
return
}
var actions: [ContextMenuItem] = []
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ScheduledMessages_SendNow, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
if let strongSelf = self {
strongSelf.controllerInteraction?.sendScheduledMessagesNow(messages.map { $0.id })
}
f(.dismissWithoutContent)
})))
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ScheduledMessages_EditTime, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Schedule"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
if let strongSelf = self {
strongSelf.controllerInteraction?.editScheduledMessagesTime(messages.map { $0.id })
}
f(.dismissWithoutContent)
})))
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak self] controller, f in
if let strongSelf = self {
strongSelf.interfaceInteraction?.deleteMessages(messages.map { $0._asMessage() }, controller, f)
}
})))
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatController: strongSelf, chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: message._asMessage(), selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil)
strongSelf.currentContextController = controller
strongSelf.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
strongSelf.window?.presentInGlobalOverlay(controller)
})
} else {
let _ = (strongSelf.context.engine.messages.failedMessageGroup(id: id)
|> deliverOnMainQueue).startStandalone(next: { messages in
guard let strongSelf = self else {
return
}
var groups: [UInt32: [Message]] = [:]
var notGrouped: [Message] = []
for message in messages {
if let groupInfo = message.groupInfo {
if groups[groupInfo.stableId] == nil {
groups[groupInfo.stableId] = []
}
groups[groupInfo.stableId]?.append(message._asMessage())
} else {
notGrouped.append(message._asMessage())
}
}
let totalGroupCount = notGrouped.count + groups.count
var maybeSelectedGroup: [Message]?
for (_, group) in groups {
if group.contains(where: { $0.id == id}) {
maybeSelectedGroup = group
break
}
}
for message in notGrouped {
if message.id == id {
maybeSelectedGroup = [message]
}
}
guard let selectedGroup = maybeSelectedGroup, let topMessage = selectedGroup.first else {
return
}
var actions: [ContextMenuItem] = []
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_MessageDialogRetry, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
if let strongSelf = self {
let _ = resendMessages(account: strongSelf.context.account, messageIds: selectedGroup.map({ $0.id })).startStandalone()
}
f(.dismissWithoutContent)
})))
if totalGroupCount != 1 {
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_MessageDialogRetryAll(totalGroupCount).string, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
if let strongSelf = self {
let _ = resendMessages(account: strongSelf.context.account, messageIds: messages.map({ $0.id })).startStandalone()
}
f(.dismissWithoutContent)
})))
}
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak self] controller, f in
if let strongSelf = self {
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: [id], type: .forLocalPeer).startStandalone()
}
f(.dismissWithoutContent)
})))
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatController: strongSelf, chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: topMessage, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil)
strongSelf.currentContextController = controller
strongSelf.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
strongSelf.window?.presentInGlobalOverlay(controller)
})
}
}, addContact: { [weak self] phoneNumber in
if let strongSelf = self {
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in
self?.present(controller, in: .window(.root), with: arguments)
}, pushController: { [weak self] controller in
if let strongSelf = self {
strongSelf.effectiveNavigationController?.pushViewController(controller)
}
}, completed: {})
})
}
}, rateCall: { [weak self] message, callId, isVideo in
if let strongSelf = self {
let controller = callRatingController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, callId: callId, userInitiated: true, isVideo: isVideo, present: { [weak self] c, a in
if let strongSelf = self {
strongSelf.present(c, in: .window(.root), with: a)
}
}, push: { [weak self] c in
if let strongSelf = self {
strongSelf.push(c)
}
})
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(controller, in: .window(.root))
}
}, requestSelectMessagePollOptions: { [weak self] id, opaqueIdentifiers in
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
return
}
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_PollUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
if controllerInteraction.pollActionState.pollMessageIdsInProgress[id] == nil {
controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifiers
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
let disposables: DisposableDict<MessageId>
if let current = strongSelf.selectMessagePollOptionDisposables {
disposables = current
} else {
disposables = DisposableDict()
strongSelf.selectMessagePollOptionDisposables = disposables
}
let signal = strongSelf.context.engine.messages.requestMessageSelectPollOption(messageId: id, opaqueIdentifiers: opaqueIdentifiers)
disposables.set((signal
|> deliverOnMainQueue).startStrict(next: { resultPoll in
guard let strongSelf = self, let resultPoll = resultPoll else {
return
}
guard let _ = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) else {
return
}
switch resultPoll.kind {
case .poll:
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.success()
case .quiz:
if let voters = resultPoll.results.voters {
for voter in voters {
if voter.selected {
if voter.isCorrect {
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.success()
strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected()
} else {
var found = false
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if !found, let itemNode = itemNode as? ChatMessageBubbleItemNode, itemNode.item?.message.id == id {
found = true
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.error()
itemNode.animateQuizInvalidOptionSelected()
if let solution = resultPoll.results.solution {
for contentNode in itemNode.contentNodes {
if let contentNode = contentNode as? ChatMessagePollBubbleContentNode {
let sourceNode = contentNode.solutionTipSourceNode
strongSelf.displayPollSolution(solution: solution, sourceNode: sourceNode, isAutomatic: true)
}
}
}
}
}
}
break
}
}
}
}
}, error: { _ in
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
return
}
if controllerInteraction.pollActionState.pollMessageIdsInProgress.removeValue(forKey: id) != nil {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
}
}, completed: {
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
return
}
if controllerInteraction.pollActionState.pollMessageIdsInProgress.removeValue(forKey: id) != nil {
Queue.mainQueue().after(1.0, {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
})
}
}), forKey: id)
}
}, requestOpenMessagePollResults: { [weak self] messageId, pollId in
guard let strongSelf = self, pollId.namespace == Namespaces.Media.CloudPoll else {
return
}
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> deliverOnMainQueue).startStandalone(next: { message in
guard let message = message else {
return
}
for media in message.media {
if let poll = media as? TelegramMediaPoll, poll.pollId == pollId {
strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, message: message, poll: poll))
break
}
}
})
}, delay: true)
}, openAppStorePage: { [weak self] in
if let strongSelf = self {
strongSelf.context.sharedContext.applicationBindings.openAppStorePage()
}
}, displayMessageTooltip: { [weak self] messageId, text, isFactCheck, node, nodeRect in
if let strongSelf = self {
if let node = node {
strongSelf.messageTooltipController?.dismiss()
let padding: CGFloat
let timeout: Double
let balancedTextLayout: Bool
let alignment: TooltipController.Alignment
let innerPadding: UIEdgeInsets
if isFactCheck {
timeout = 5.0
padding = 20.0
balancedTextLayout = true
alignment = .natural
innerPadding = UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0)
} else {
timeout = 2.0
padding = 8.0
balancedTextLayout = false
alignment = .center
innerPadding = .zero
}
let tooltipController = TooltipController(content: .text(text), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, balancedTextLayout: balancedTextLayout, alignment: alignment, isBlurred: true, timeout: timeout, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true, padding: padding, innerPadding: innerPadding)
strongSelf.messageTooltipController = tooltipController
tooltipController.dismissed = { [weak tooltipController] _ in
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.messageTooltipController === tooltipController {
strongSelf.messageTooltipController = nil
}
}
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
if let strongSelf = self {
var rect = node.view.convert(node.view.bounds, to: strongSelf.chatDisplayNode.view)
if let nodeRect = nodeRect {
rect = CGRect(origin: rect.origin.offsetBy(dx: nodeRect.minX, dy: nodeRect.minY - node.bounds.minY), size: nodeRect.size)
}
return (strongSelf.chatDisplayNode, rect)
}
return nil
}))
}
}
}, seekToTimecode: { [weak self] message, timestamp, forceOpen in
if let strongSelf = self {
var found = false
if !forceOpen {
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if !found, let itemNode = itemNode as? ChatMessageItemView, itemNode.item?.message.id == message.id, let (action, _, _, _, _) = itemNode.playMediaWithSound() {
if case let .visible(fraction, _) = itemNode.visibility, fraction > 0.7 {
action(Double(timestamp))
} else {
let _ = strongSelf.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .timecode(Double(timestamp))))
}
found = true
}
}
}
if !found {
var messageId = message.id
if let forwardInfo = message.forwardInfo, let sourceMessageId = forwardInfo.sourceMessageId, case let .replyThread(threadMessage) = strongSelf.chatLocation, threadMessage.isChannelPost {
messageId = sourceMessageId
}
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
let _ = strongSelf.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .timecode(Double(timestamp))))
} else {
strongSelf.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: Double(timestamp), quote: nil)), animated: true, forceInCurrentChat: true)
}
}
}
}, scheduleCurrentMessage: { [weak self] params in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(completion: { [weak self] time in
if let strongSelf = self {
if let _ = strongSelf.presentationInterfaceState.interfaceState.mediaDraftState {
strongSelf.sendMediaRecording(scheduleTime: time, messageEffect: (params?.effect).flatMap {
return ChatSendMessageEffect(id: $0.id)
})
} else {
let silentPosting = strongSelf.presentationInterfaceState.interfaceState.silentPosting
strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, scheduleTime: time, messageEffect: (params?.effect).flatMap {
return ChatSendMessageEffect(id: $0.id)
}) { [weak self] in
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: strongSelf.presentationInterfaceState.subject != .scheduledMessages, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) }
})
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
}
}
}
})
}
}, sendScheduledMessagesNow: { [weak self] messageIds in
if let strongSelf = self {
if let _ = strongSelf.presentationInterfaceState.slowmodeState {
if let rect = strongSelf.chatDisplayNode.frameForInputActionButton() {
strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode.view, rect)
}
return
} else {
let _ = strongSelf.context.engine.messages.sendScheduledMessageNowInteractively(messageId: messageIds.first!).startStandalone()
}
}
}, editScheduledMessagesTime: { [weak self] messageIds in
if let strongSelf = self, let messageId = messageIds.first {
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] message in
guard let strongSelf = self, let message = message else {
return
}
strongSelf.presentScheduleTimePicker(selectedTime: message.timestamp, completion: { [weak self] time in
if let strongSelf = self {
var entities: TextEntitiesMessageAttribute?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entities = attribute
break
}
}
let inlineStickers: [MediaId: TelegramMediaFile] = [:]
strongSelf.editMessageDisposable.set((strongSelf.context.engine.messages.requestEditMessage(messageId: messageId, text: message.text, media: .keep, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: nil, disableUrlPreview: false, scheduleTime: time) |> deliverOnMainQueue).startStrict(next: { result in
}, error: { error in
}))
}
})
})
}, delay: true)
}
}, performTextSelectionAction: { [weak self] message, canCopy, text, action in
guard let strongSelf = self else {
return
}
if let performTextSelectionAction = strongSelf.performTextSelectionAction {
performTextSelectionAction(message, canCopy, text, action)
return
}
switch action {
case .copy:
storeAttributedTextInPasteboard(text)
case .share:
let f = {
guard let strongSelf = self else {
return
}
let shareController = ShareController(context: strongSelf.context, subject: .text(text.string), externalShare: true, immediateExternalShare: false, updatedPresentationData: strongSelf.updatedPresentationData)
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(shareController, in: .window(.root))
}
if let currentContextController = strongSelf.currentContextController {
currentContextController.dismiss(completion: {
f()
})
} else {
f()
}
case .lookup:
let controller = UIReferenceLibraryViewController(term: text.string)
if let window = strongSelf.effectiveNavigationController?.view.window {
controller.popoverPresentationController?.sourceView = window
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
window.rootViewController?.present(controller, animated: true)
}
case .speak:
if let speechHolder = speakText(context: strongSelf.context, text: text.string) {
speechHolder.completion = { [weak self, weak speechHolder] in
if let strongSelf = self, strongSelf.currentSpeechHolder == speechHolder {
strongSelf.currentSpeechHolder = nil
}
}
strongSelf.currentSpeechHolder = speechHolder
}
case .translate:
strongSelf.chatDisplayNode.dismissInput()
let f = {
let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] sharedData in
guard let strongSelf = self else {
return
}
let translationSettings: TranslationSettings
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
translationSettings = current
} else {
translationSettings = TranslationSettings.defaultSettings
}
var showTranslateIfTopical = false
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, !(peer.addressName ?? "").isEmpty {
showTranslateIfTopical = true
}
let (_, language) = canTranslateText(context: context, text: text.string, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages)
let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: context.sharedContext.accountManager, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone()
let controller = TranslateScreen(context: context, text: text.string, canCopy: canCopy, fromLanguage: language, ignoredLanguages: translationSettings.ignoredLanguages)
controller.pushController = { [weak self] c in
self?.effectiveNavigationController?._keepModalDismissProgress = true
self?.push(c)
}
controller.presentController = { [weak self] c in
self?.present(c, in: .window(.root))
}
strongSelf.present(controller, in: .window(.root))
})
}
if let currentContextController = strongSelf.currentContextController {
currentContextController.dismiss(completion: {
f()
})
} else {
f()
}
case let .quote(range):
let completion: (ContainedViewLayoutTransition?) -> Void = { transition in
guard let self else {
return
}
if let currentContextController = self.currentContextController {
self.currentContextController = nil
if let transition {
currentContextController.dismissWithCustomTransition(transition: transition)
} else {
currentContextController.dismiss(completion: {})
}
}
}
if let messageId = message?.id, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) ?? message {
var quoteData: EngineMessageReplyQuote?
let nsRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)
let quoteText = (message.text as NSString).substring(with: nsRange)
let trimmedText = trimStringWithEntities(string: quoteText, entities: messageTextEntitiesInRange(entities: message.textEntitiesAttribute?.entities ?? [], range: nsRange, onlyQuoteable: true), maxLength: quoteMaxLength(appConfig: strongSelf.context.currentAppConfiguration.with({ $0 })))
if !trimmedText.string.isEmpty {
quoteData = EngineMessageReplyQuote(text: trimmedText.string, offset: nsRange.location, entities: trimmedText.entities, media: nil)
}
let replySubject = ChatInterfaceState.ReplyMessageSubject(
messageId: message.id,
quote: quoteData
)
if canSendMessagesToChat(strongSelf.presentationInterfaceState) {
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject) }).updatedSearch(nil).updatedShowCommands(false) }, completion: completion)
strongSelf.updateItemNodesSearchTextHighlightStates()
strongSelf.chatDisplayNode.ensureInputViewFocused()
}, alertAction: {
completion(nil)
}, delay: true)
} else {
moveReplyMessageToAnotherChat(selfController: strongSelf, replySubject: replySubject)
completion(nil)
}
} else {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }) }, completion: completion)
}
}
}, displayImportedMessageTooltip: { [weak self] _ in
guard let strongSelf = self else {
return
}
if let _ = strongSelf.currentImportMessageTooltip {
} else {
let controller = UndoOverlayController(presentationData: strongSelf.presentationData, content: .importedMessage(text: strongSelf.presentationData.strings.Conversation_ImportedMessageHint), elevatedLayout: false, action: { _ in return false })
strongSelf.currentImportMessageTooltip = controller
strongSelf.present(controller, in: .current)
}
}, displaySwipeToReplyHint: { [weak self] in
if let strongSelf = self, let validLayout = strongSelf.validLayout, min(validLayout.size.width, validLayout.size.height) > 320.0 {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: false, position: .top, action: { _ in return false }), in: .current)
}
}, dismissReplyMarkupMessage: { [weak self] message in
guard let strongSelf = self, strongSelf.presentationInterfaceState.keyboardButtonsMessage?.id == message.id else {
return
}
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedInputMode({ _ in .text }).updatedInterfaceState({
$0.withUpdatedMessageActionsState({ value in
var value = value
value.closedButtonKeyboardMessageId = message.id
value.dismissedButtonKeyboardMessageId = message.id
return value
})
})
})
}, openMessagePollResults: { [weak self] messageId, optionOpaqueIdentifier in
guard let strongSelf = self else {
return
}
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> deliverOnMainQueue).startStandalone(next: { message in
guard let message = message else {
return
}
for media in message.media {
if let poll = media as? TelegramMediaPoll, poll.pollId.namespace == Namespaces.Media.CloudPoll {
strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, message: message, poll: poll, focusOnOptionWithOpaqueIdentifier: optionOpaqueIdentifier))
break
}
}
})
})
}, openPollCreation: { [weak self] isQuiz in
guard let strongSelf = self else {
return
}
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
if let controller = strongSelf.configurePollCreation(isQuiz: isQuiz) {
strongSelf.effectiveNavigationController?.pushViewController(controller)
}
})
}, displayPollSolution: { [weak self] solution, sourceNode in
self?.displayPollSolution(solution: solution, sourceNode: sourceNode, isAutomatic: false)
}, displayPsa: { [weak self] type, sourceNode in
self?.displayPsa(type: type, sourceNode: sourceNode, isAutomatic: false)
}, displayDiceTooltip: { [weak self] dice in
self?.displayDiceTooltip(dice: dice)
}, animateDiceSuccess: { [weak self] haptic, confetti in
guard let strongSelf = self else {
return
}
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
if haptic {
strongSelf.selectPollOptionFeedback?.success()
}
if confetti {
strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected()
}
}, displayPremiumStickerTooltip: { [weak self] file, message in
self?.displayPremiumStickerTooltip(file: file, message: message)
}, displayEmojiPackTooltip: { [weak self] file, message in
self?.displayEmojiPackTooltip(file: file, message: message)
}, openPeerContextMenu: { [weak self] peer, messageId, node, rect, gesture in
guard let strongSelf = self else {
return
}
if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil {
return
}
strongSelf.dismissAllTooltips()
let context = strongSelf.context
let dataSignal: Signal<(EnginePeer?, EngineMessage?), NoError>
if let messageId = messageId {
dataSignal = context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id),
TelegramEngine.EngineData.Item.Messages.Message(id: messageId)
)
} else {
dataSignal = context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
)
|> map { peer -> (EnginePeer?, EngineMessage?) in
return (peer, nil)
}
}
let _ = (dataSignal
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer, message in
guard let strongSelf = self, let peer = peer, peer.smallProfileImage != nil else {
return
}
let galleryController = AvatarGalleryController(context: context, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in
}, synchronousLoad: true)
galleryController.setHintWillBePresentedInPreviewingContext(true)
var isChannel = false
if case let .channel(peer) = peer, case .broadcast = peer.info {
isChannel = true
}
var items: [ContextMenuItem] = [
.action(ContextMenuActionItem(text: isChannel ? strongSelf.presentationData.strings.Conversation_ContextMenuOpenChannelProfile : strongSelf.presentationData.strings.Conversation_ContextMenuOpenProfile, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
self?.openPeer(peer: peer, navigation: .info(nil), fromMessage: nil)
}))
]
items.append(.action(ContextMenuActionItem(text: isChannel ? strongSelf.presentationData.strings.Conversation_ContextMenuOpenChannel : strongSelf.presentationData.strings.Conversation_ContextMenuSendMessage, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isChannel ? "Chat/Context Menu/Channels" : "Chat/Context Menu/Message"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
self?.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil)
})))
if !isChannel && canSendMessagesToChat(strongSelf.presentationInterfaceState) {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuMention, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Mention"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
var inputMode = inputMode
if inputMode == .none {
inputMode = .text
}
return (chatTextInputAddMentionAttribute(current, peer: peer), inputMode)
}
}, delay: true)
})))
}
if !isChannel {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuSearchMessages, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Search"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
strongSelf.activateSearch(domain: .member(peer._asPeer()))
})))
}
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
strongSelf.canReadHistory.set(false)
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
contextController.dismissed = { [weak self] in
self?.canReadHistory.set(true)
}
strongSelf.presentInGlobalOverlay(contextController)
})
}, openMessageReplies: { [weak self] messageId, isChannelPost, displayModalProgress in
guard let strongSelf = self else {
return
}
strongSelf.openMessageReplies(messageId: messageId, displayProgressInMessage: displayModalProgress ? nil : messageId, isChannelPost: isChannelPost, atMessage: nil, displayModalProgress: displayModalProgress)
}, openReplyThreadOriginalMessage: { [weak self] message in
guard let strongSelf = self else {
return
}
var threadMessageId: MessageId?
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
threadMessageId = attribute.threadMessageId
break
}
}
for attribute in message.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if let threadMessageId = threadMessageId {
if let _ = strongSelf.navigationController as? NavigationController {
strongSelf.openMessageReplies(messageId: threadMessageId, displayProgressInMessage: message.id, isChannelPost: true, atMessage: attribute.messageId, displayModalProgress: false)
}
} else {
strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)))
}
break
}
}
}, openMessageStats: { [weak self] id in
guard let strongSelf = self else {
return
}
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: id))
|> mapToSignal { message -> Signal<EngineMessage.Id?, NoError> in
if let message {
return .single(message.id)
} else {
return .complete()
}
}
|> deliverOnMainQueue).startStandalone(next: { [weak self] messageId in
guard let strongSelf = self, let messageId else {
return
}
strongSelf.push(messageStatsController(context: context, subject: .message(id: messageId)))
})
}, delay: true)
}, editMessageMedia: { [weak self] messageId, draw in
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.dismissInput()
if draw {
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] message in
guard let strongSelf = self, let message = message else {
return
}
var mediaReference: AnyMediaReference?
for m in message.media {
if let image = m as? TelegramMediaImage {
mediaReference = AnyMediaReference.standalone(media: image)
}
}
if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] {
let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText
legacyMediaEditor(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, media: mediaReference, mode: .draw, initialCaption: inputText, snapshots: [], transitionCompletion: nil, getCaptionPanelView: { [weak self] in
return self?.getCaptionPanelView(isFile: true)
}, sendMessagesWithSignals: { [weak self] signals, _, _ in
if let strongSelf = self {
strongSelf.interfaceInteraction?.setupEditMessage(messageId, { _ in })
strongSelf.editMessageMediaWithLegacySignals(signals!)
}
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
})
}
})
} else {
strongSelf.presentOldMediaPicker(fileMode: false, editingMedia: true, completion: { signals, _, _ in
self?.interfaceInteraction?.setupEditMessage(messageId, { _ in })
self?.editMessageMediaWithLegacySignals(signals)
})
}
}, copyText: { [weak self] text in
if let strongSelf = self {
storeMessageTextInPasteboard(text, entities: nil)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
return true
}), in: .current)
}
}, displayUndo: { [weak self] content in
if let strongSelf = self {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
strongSelf.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
})
strongSelf.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
return true
})
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in
return true
}), in: .current)
}
}, isAnimatingMessage: { [weak self] stableId in
guard let strongSelf = self else {
return false
}
return strongSelf.chatDisplayNode.messageTransitionNode.isAnimatingMessage(stableId: stableId)
}, getMessageTransitionNode: { [weak self] in
guard let strongSelf = self else {
return nil
}
return strongSelf.chatDisplayNode.messageTransitionNode
}, updateChoosingSticker: { [weak self] value in
if let strongSelf = self {
strongSelf.choosingStickerActivityPromise.set(value)
}
}, commitEmojiInteraction: { [weak self] messageId, emoji, interaction, file in
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, peer.id != strongSelf.context.account.peerId else {
return
}
strongSelf.context.account.updateLocalInputActivity(peerId: PeerActivitySpace(peerId: messageId.peerId, category: .global), activity: .interactingWithEmoji(emoticon: emoji, messageId: messageId, interaction: interaction), isPresent: true)
let currentTimestamp = Int32(Date().timeIntervalSince1970)
let _ = (ApplicationSpecificNotice.getInteractiveEmojiSyncTip(accountManager: strongSelf.context.sharedContext.accountManager)
|> deliverOnMainQueue).startStandalone(next: { [weak self] count, timestamp in
if let strongSelf = self, count < 3 && currentTimestamp > timestamp + 24 * 60 * 60 {
strongSelf.interactiveEmojiSyncDisposable.set(
(strongSelf.peerInputActivitiesPromise.get()
|> filter { activities -> Bool in
var found = false
for (_, activity) in activities {
if case .seeingEmojiInteraction(emoji) = activity {
found = true
break
}
}
return found
}
|> map { _ -> Bool in
return true
}
|> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(false))).startStrict(next: { [weak self] responded in
if let strongSelf = self {
if !responded {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: nil, text: strongSelf.presentationData.strings.Conversation_InteractiveEmojiSyncTip(EnginePeer(peer).compactDisplayTitle).string, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
let _ = ApplicationSpecificNotice.incrementInteractiveEmojiSyncTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).startStandalone()
}
}
})
)
}
})
}, openLargeEmojiInfo: { [weak self] _, fitz, file in
guard let strongSelf = self else {
return
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
LargeEmojiActionSheetItem(context: strongSelf.context, text: strongSelf.presentationData.strings.Conversation_LargeEmojiDisabledInfo, fitz: fitz, file: file),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LargeEmojiEnable, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let _ = updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in
return current.withUpdatedLargeEmoji(true)
}).startStandalone()
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .emoji(name: "TwoFactorSetupRememberSuccess", text: strongSelf.presentationData.strings.Conversation_LargeEmojiEnabled), elevatedLayout: false, action: { _ in return false }), in: .current)
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(actionSheet, in: .window(.root))
}, openJoinLink: { [weak self] joinHash in
guard let strongSelf = self else {
return
}
strongSelf.openResolved(result: .join(joinHash), sourceMessageId: nil)
}, openWebView: { [weak self] buttonText, url, simple, source in
guard let self else {
return
}
self.openWebApp(buttonText: buttonText, url: url, simple: simple, source: source)
}, activateAdAction: { [weak self] messageId, progress in
guard let self, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let adAttribute = message.adAttribute else {
return
}
self.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId)
self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: adAttribute.url, concealed: false, external: true, progress: progress))
}, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId, maxQuantity in
guard let self else {
return
}
let botName = self.presentationInterfaceState.renderedPeer?.peer.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
let context = self.context
let peerId = self.chatLocation.peerId
let presentConfirmation: (String, Bool, @escaping () -> Void) -> Void = { [weak self] peerName, isChannel, completion in
guard let strongSelf = self else {
return
}
var attributedTitle: NSAttributedString?
let attributedText: NSAttributedString
let theme = AlertControllerTheme(presentationData: strongSelf.presentationData)
if case .user = peerType {
attributedTitle = nil
attributedText = NSAttributedString(string: strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationTitle(peerName, botName).string, font: Font.medium(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
} else {
attributedTitle = NSAttributedString(string: strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationTitle(peerName, botName).string, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
var botAdminRights: TelegramChatAdminRights?
switch peerType {
case let .group(group):
botAdminRights = group.botAdminRights
case let .channel(channel):
botAdminRights = channel.botAdminRights
default:
break
}
if let botAdminRights {
if botAdminRights.rights.isEmpty {
let stringWithRanges = strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationInviteAdminText(botName, peerName)
let formattedString = NSMutableAttributedString(string: stringWithRanges.string, font: Font.regular(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
for range in stringWithRanges.ranges.prefix(2) {
formattedString.addAttribute(.font, value: Font.semibold(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), range: range.range)
}
attributedText = formattedString
} else {
let stringWithRanges = strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationInviteWithRightsText(botName, peerName, stringForAdminRights(strings: strongSelf.presentationData.strings, adminRights: botAdminRights, isChannel: isChannel))
let formattedString = NSMutableAttributedString(string: stringWithRanges.string, font: Font.regular(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
for range in stringWithRanges.ranges.prefix(2) {
formattedString.addAttribute(.font, value: Font.semibold(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), range: range.range)
}
attributedText = formattedString
}
} else {
if case let .group(group) = peerType, group.botParticipant {
let stringWithRanges = strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationInviteText(botName, peerName)
let formattedString = NSMutableAttributedString(string: stringWithRanges.string, font: Font.regular(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
for range in stringWithRanges.ranges.prefix(2) {
formattedString.addAttribute(.font, value: Font.semibold(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), range: range.range)
}
attributedText = formattedString
} else {
attributedTitle = nil
attributedText = NSAttributedString(string: strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationTitle(peerName, botName).string, font: Font.semibold(strongSelf.presentationData.listsFontSize.baseDisplaySize), textColor: theme.primaryColor, paragraphAlignment: .center)
}
}
}
let controller = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationSend, action: {
completion()
})])
strongSelf.present(controller, in: .window(.root))
}
if case .user = peerType, maxQuantity > 1 {
let presentationData = self.presentationData
var reachedLimitImpl: ((Int32) -> Void)?
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .requestedUsersSelection, isPeerEnabled: { peer in
if case let .user(user) = peer, user.botInfo == nil {
return true
} else {
return false
}
}, limit: maxQuantity, reachedLimit: { limit in
reachedLimitImpl?(limit)
}))
controller.navigationPresentation = .modal
reachedLimitImpl = { [weak controller] limit in
guard let controller else {
return
}
HapticFeedback().error()
controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.RequestPeer_ReachedMaximum(limit), timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
let _ = (controller.result
|> deliverOnMainQueue).startStandalone(next: { [weak controller] result in
guard let controller else {
return
}
var peerIds: [PeerId] = []
if case let .result(peerIdsValue, _) = result {
peerIds = peerIdsValue.compactMap({ peerId in
if case let .peer(peerId) = peerId {
return peerId
} else {
return nil
}
})
}
let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerIds: peerIds).startStandalone()
controller.dismiss()
})
self.push(controller)
} else {
var createNewGroupImpl: (() -> Void)?
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: [peerType], hasContactSelector: false, createNewGroup: {
createNewGroupImpl?()
}, hasCreation: true))
controller.peerSelected = { [weak self, weak controller] peer, _ in
guard let strongSelf = self else {
return
}
if case .user = peerType {
let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerIds: [peer.id]).startStandalone()
controller?.dismiss()
} else {
var isChannel = false
if case let .channel(channel) = peer, case .broadcast = channel.info {
isChannel = true
}
let peerName = peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)
presentConfirmation(peerName, isChannel, {
let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerIds: [peer.id]).startStandalone()
controller?.dismiss()
})
}
}
createNewGroupImpl = { [weak controller] in
switch peerType {
case .user:
break
case let .group(group):
let createGroupController = createGroupControllerImpl(context: context, peerIds: group.botParticipant || group.botAdminRights != nil ? (peerId.flatMap { [$0] } ?? []) : [], mode: .requestPeer(group), willComplete: { peerName, complete in
presentConfirmation(peerName, false, {
complete()
})
}, completion: { peerId, dismiss in
let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerIds: [peerId]).startStandalone()
dismiss()
})
createGroupController.navigationPresentation = .modal
controller?.replace(with: createGroupController)
case let .channel(channel):
let createChannelController = createChannelController(context: context, mode: .requestPeer(channel), willComplete: { peerName, complete in
presentConfirmation(peerName, true, {
complete()
})
}, completion: { peerId, dismiss in
let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerIds: [peerId]).startStandalone()
dismiss()
})
createChannelController.navigationPresentation = .modal
controller?.replace(with: createChannelController)
}
}
self.push(controller)
}
}, saveMediaToFiles: { [weak self] messageId in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> deliverOnMainQueue).startStandalone(next: { message in
guard let self, let message else {
return
}
var file: TelegramMediaFile?
var title: String?
var performer: String?
for media in message.media {
if let mediaFile = media as? TelegramMediaFile, mediaFile.isMusic {
file = mediaFile
for attribute in mediaFile.attributes {
if case let .Audio(_, _, titleValue, performerValue, _) = attribute {
if let titleValue, !titleValue.isEmpty {
title = titleValue
}
if let performerValue, !performerValue.isEmpty {
performer = performerValue
}
}
}
}
}
guard let file else {
return
}
var signal = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: .message(message: MessageReference(message._asMessage()), media: file))
let disposable: MetaDisposable
if let current = self.saveMediaDisposable {
disposable = current
} else {
disposable = MetaDisposable()
self.saveMediaDisposable = disposable
}
var cancelImpl: (() -> Void)?
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
guard let self else {
return EmptyDisposable
}
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.startStrict()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = { [weak disposable] in
disposable?.set(nil)
}
disposable.set((signal
|> deliverOnMainQueue).startStrict(next: { [weak self] state, _ in
guard let self else {
return
}
switch state {
case .progress:
break
case let .data(data):
if data.complete {
var symlinkPath = data.path + ".mp3"
if fileSize(symlinkPath) != nil {
try? FileManager.default.removeItem(atPath: symlinkPath)
}
let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath)
let audioUrl = URL(fileURLWithPath: symlinkPath)
let audioAsset = AVURLAsset(url: audioUrl)
var fileExtension = "mp3"
if let filename = file.fileName {
if let dotIndex = filename.lastIndex(of: ".") {
fileExtension = String(filename[filename.index(after: dotIndex)...])
}
}
var nameComponents: [String] = []
if let title {
if let performer {
nameComponents.append(performer)
}
nameComponents.append(title)
} else {
var artist: String?
var title: String?
for data in audioAsset.commonMetadata {
if data.commonKey == .commonKeyArtist {
artist = data.stringValue
}
if data.commonKey == .commonKeyTitle {
title = data.stringValue
}
}
if let artist, !artist.isEmpty {
nameComponents.append(artist)
}
if let title, !title.isEmpty {
nameComponents.append(title)
}
if nameComponents.isEmpty, var filename = file.fileName {
if let dotIndex = filename.lastIndex(of: ".") {
filename = String(filename[..<dotIndex])
}
nameComponents.append(filename)
}
}
if !nameComponents.isEmpty {
try? FileManager.default.removeItem(atPath: symlinkPath)
let fileName = "\(nameComponents.joined(separator: " ")).\(fileExtension)"
symlinkPath = symlinkPath.replacingOccurrences(of: audioUrl.lastPathComponent, with: fileName)
let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath)
}
let url = URL(fileURLWithPath: symlinkPath)
let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in
})
self.present(controller, in: .window(.root))
}
}
}))
})
}, openNoAdsDemo: { [weak self] in
guard let self else {
return
}
if self.context.isPremium {
self.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.presentationData.strings.ReportAd_Hidden, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in
return true
}), in: .current)
var adOpaqueId: Data?
self.chatDisplayNode.historyNode.forEachVisibleMessageItemNode { itemView in
if let adAttribute = itemView.item?.message.adAttribute {
adOpaqueId = adAttribute.opaqueId
}
}
let _ = self.context.engine.accountData.updateAdMessagesEnabled(enabled: false).start()
if let adOpaqueId {
self.removeAd(opaqueId: adOpaqueId)
}
} else {
let context = self.context
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
self.push(controller)
}
}, openAdsInfo: { [weak self] in
guard let self else {
return
}
self.push(AdsInfoScreen(context: self.context))
}, displayGiveawayParticipationStatus: { [weak self] messageId in
guard let self else {
return
}
let disposable: MetaDisposable
if let current = self.giveawayStatusDisposable {
disposable = current
} else {
disposable = MetaDisposable()
self.giveawayStatusDisposable = disposable
}
disposable.set((self.context.engine.payments.premiumGiveawayInfo(peerId: messageId.peerId, messageId: messageId)
|> deliverOnMainQueue).start(next: { [weak self] info in
guard let self, let info else {
return
}
let content: UndoOverlayContent
switch info {
case let .ongoing(_, status):
switch status {
case .notAllowed:
content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_NotAllowed, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore)
case .participating:
content = .succeed(text: self.presentationData.strings.Chat_Giveaway_Toast_Participating, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore)
case .notQualified:
content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_NotQualified, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore)
case .almostOver:
content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_AlmostOver, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore)
}
case .finished:
content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_Ended, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore)
}
let controller = UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { [weak self] action in
if case .undo = action, let self {
self.displayGiveawayStatusInfo(messageId: messageId, giveawayInfo: info)
return true
}
return false
})
self.present(controller, in: .current)
}))
}, openPremiumStatusInfo: { [weak self] peerId, sourceView, peerStatus, nameColor in
guard let self else {
return
}
let context = self.context
let source: Signal<PremiumSource, NoError>
if let peerStatus {
source = context.engine.stickers.resolveInlineStickers(fileIds: [peerStatus])
|> mapToSignal { files in
if let file = files[peerStatus] {
var reference: StickerPackReference?
for attribute in file.attributes {
if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference {
reference = packReference
break
}
}
if let reference {
return context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false)
|> filter { result in
if case .result = result {
return true
} else {
return false
}
}
|> take(1)
|> mapToSignal { result -> Signal<PremiumSource, NoError> in
if case let .result(_, items, _) = result {
return .single(.emojiStatus(peerId, peerStatus, items.first?.file, result))
} else {
return .single(.emojiStatus(peerId, peerStatus, nil, nil))
}
}
} else {
return .single(.emojiStatus(peerId, peerStatus, nil, nil))
}
} else {
return .single(.emojiStatus(peerId, peerStatus, nil, nil))
}
}
} else {
source = .single(.profile(peerId))
}
let _ = (source
|> deliverOnMainQueue).startStandalone(next: { [weak self] source in
guard let self else {
return
}
let controller = PremiumIntroScreen(context: self.context, source: source)
controller.sourceView = sourceView
controller.containerView = self.navigationController?.view
controller.animationColor = self.context.peerNameColors.get(nameColor, dark: self.presentationData.theme.overallDarkAppearance).main
self.push(controller)
})
}, openRecommendedChannelContextMenu: { [weak self] peer, sourceView, gesture in
guard let self else {
return
}
let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil)
chatController.canReadHistory.set(false)
var items: [ContextMenuItem] = [
.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_LinkDialogOpen, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ImageEnlarge"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil)
})),
]
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_SimilarChannels_Join, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self else {
return
}
let presentationData = self.presentationData
self.joinChannelDisposable.set((
self.context.peerChannelMemberCategoriesContextsManager.join(engine: self.context.engine, peerId: peer.id, hash: nil)
|> deliverOnMainQueue
|> afterCompleted { [weak self] in
Queue.mainQueue().async {
if let self {
self.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Chat_SimilarChannels_JoinedChannel(peer.compactDisplayTitle).string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
}
}
).startStrict(error: { [weak self] error in
guard let self else {
return
}
let text: String
switch error {
case .inviteRequestSent:
self.present(UndoOverlayController(presentationData: presentationData, content: .inviteRequestSent(title: presentationData.strings.Group_RequestToJoinSent, text: presentationData.strings.Group_RequestToJoinSentDescriptionGroup), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
return
case .tooMuchJoined:
self.push(oldChannelsController(context: context, intent: .join))
return
case .tooMuchUsers:
text = self.presentationData.strings.Conversation_UsersTooMuchError
case .generic:
text = self.presentationData.strings.Channel_ErrorAccessDenied
}
self.present(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}))
})))
self.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
self.canReadHistory.set(false)
let contextController = ContextController(presentationData: self.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceView: sourceView, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
contextController.dismissed = { [weak self] in
self?.canReadHistory.set(true)
}
self.presentInGlobalOverlay(contextController)
}, openGroupBoostInfo: { [weak self] userId, count in
guard let self, let peerId = self.chatLocation.peerId else {
return
}
let _ = combineLatest(queue: Queue.mainQueue(),
context.engine.peers.getChannelBoostStatus(peerId: peerId),
context.engine.peers.getMyBoostStatus()
).startStandalone(next: { [weak self] boostStatus, myBoostStatus in
guard let self, let boostStatus, let myBoostStatus else {
return
}
let boostController = PremiumBoostLevelsScreen(
context: self.context,
peerId: peerId,
mode: userId.flatMap { .user(mode: .groupPeer($0, count)) } ?? .user(mode: .current),
status: boostStatus,
myBoostStatus: myBoostStatus
)
self.push(boostController)
})
}, openStickerEditor: { [weak self] in
guard let self else {
return
}
self.openStickerEditor()
}, openAgeRestrictedMessageMedia: { [weak self] message, reveal in
guard let self else {
return
}
let controller = chatAgeRestrictionAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, completion: { [weak self] alwaysShow in
guard let self else {
return
}
if alwaysShow {
self.present(UndoOverlayController(presentationData: self.presentationData, content: .info(title: nil, text: "You can update the visibility of sensitive media in [Data and Storage > Show 18+ Content]().", timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return false }), in: .current)
}
reveal()
})
self.present(controller, in: .window(.root))
}, playMessageEffect: { [weak self] message in
guard let self else {
return
}
self.playMessageEffect(message: message)
}, editMessageFactCheck: { [weak self] messageId in
guard let self else {
return
}
self.openEditMessageFactCheck(messageId: messageId)
}, requestMessageUpdate: { [weak self] id, scroll in
if let self {
self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll)
}
}, cancelInteractiveKeyboardGestures: { [weak self] in
if let self {
(self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures()
self.chatDisplayNode.cancelInteractiveKeyboardGestures()
}
}, dismissTextInput: { [weak self] in
self?.chatDisplayNode.dismissTextInput()
}, scrollToMessageId: { [weak self] index in
self?.chatDisplayNode.historyNode.scrollToMessage(index: index)
}, navigateToStory: { [weak self] message, storyId in
guard let self else {
return
}
if let story = message.associatedStories[storyId], story.data.isEmpty {
self.present(UndoOverlayController(presentationData: self.presentationData, content: .universal(animation: "story_expired", scale: 0.066, colors: [:], title: nil, text: self.presentationData.strings.Story_TooltipExpired, customUndoText: nil, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
return
}
let storyContent = SingleStoryContentContextImpl(context: self.context, storyId: storyId, readGlobally: true)
let _ = (storyContent.state
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] _ in
guard let self else {
return
}
var transitionIn: StoryContainerScreen.TransitionIn?
for i in 0 ..< 2 {
if transitionIn != nil {
break
}
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if i == 0 {
if itemNode.item?.message.id != message.id {
return
}
}
if let result = itemNode.targetForStoryTransition(id: storyId) {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: result,
sourceRect: result.bounds,
sourceCornerRadius: 6.0,
sourceIsAvatar: false
)
}
}
}
}
let storyContainerScreen = StoryContainerScreen(
context: self.context,
content: storyContent,
transitionIn: transitionIn,
transitionOut: { [weak self] peerId, storyIdValue in
guard let self, let storyIdId = storyIdValue.base as? Int32 else {
return nil
}
let storyId = StoryId(peerId: peerId, id: storyIdId)
var transitionOut: StoryContainerScreen.TransitionOut?
for i in 0 ..< 2 {
if transitionOut != nil {
break
}
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if i == 0 {
if itemNode.item?.message.id != message.id {
return
}
}
if let result = itemNode.targetForStoryTransition(id: storyId) {
result.isHidden = true
transitionOut = StoryContainerScreen.TransitionOut(
destinationView: result,
transitionView: StoryContainerScreen.TransitionView(
makeView: { [weak result] in
let parentView = UIView()
if let copyView = result?.snapshotContentTree(unhide: true) {
parentView.addSubview(copyView)
}
return parentView
},
updateView: { copyView, state, transition in
guard let view = copyView.subviews.first else {
return
}
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
},
insertCloneTransitionView: nil
),
destinationRect: result.bounds,
destinationCornerRadius: 2.0,
destinationIsAvatar: false,
completed: { [weak result] in
result?.isHidden = false
}
)
}
}
}
}
return transitionOut
}
)
self.push(storyContainerScreen)
})
}, attemptedNavigationToPrivateQuote: { [weak self] peer in
guard let self else {
return
}
let text: String
if let peer = peer as? TelegramChannel {
if case .broadcast = peer.info {
text = self.presentationData.strings.Chat_ToastQuoteChatUnavailbalePrivateChannel
} else {
text = self.presentationData.strings.Chat_ToastQuoteChatUnavailbalePrivateGroup
}
} else if peer is TelegramGroup {
text = self.presentationData.strings.Chat_ToastQuoteChatUnavailbalePrivateGroup
} else {
text = self.presentationData.strings.Chat_ToastQuoteChatUnavailbalePrivateChat
}
self.controllerInteraction?.displayUndo(.info(title: nil, text: text, timeout: nil, customUndoText: nil))
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode))
controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency
self.controllerInteraction = controllerInteraction
//if chatLocation.threadId == nil {
if let peerId = chatLocation.peerId, peerId != context.account.peerId {
switch subject {
case .pinnedMessages, .scheduledMessages, .messageOptions:
break
default:
self.navigationBar?.userInfo = PeerInfoNavigationSourceTag(peerId: peerId)
}
}
self.navigationBar?.allowsCustomTransition = { [weak self] in
guard let strongSelf = self else {
return false
}
if strongSelf.navigationBar?.userInfo == nil {
return false
}
if strongSelf.navigationBar?.contentNode != nil {
return false
}
return true
}
//}
self.chatTitleView = ChatTitleView(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer)
if case .messageOptions = self.subject {
self.chatTitleView?.disableAnimations = true
}
self.navigationItem.titleView = self.chatTitleView
self.chatTitleView?.longPressed = { [weak self] in
if let strongSelf = self, let peerView = strongSelf.peerView, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && !strongSelf.presentationInterfaceState.isNotAccessible {
strongSelf.interfaceInteraction?.beginMessageSearch(.everything, "")
}
}
let chatInfoButtonItem: UIBarButtonItem
switch chatLocation {
case .peer, .replyThread:
let avatarNode = ChatAvatarNavigationNode()
avatarNode.contextAction = { [weak self] node, gesture in
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, peer.smallProfileImage != nil else {
return
}
let galleryController = AvatarGalleryController(context: strongSelf.context, peer: EnginePeer(peer), remoteEntries: nil, replaceRootController: { controller, ready in
}, synchronousLoad: true)
galleryController.setHintWillBePresentedInPreviewingContext(true)
let items: Signal<[ContextMenuItem], NoError> = context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.CanViewStats(id: peer.id)
)
|> map { canViewStats -> [ContextMenuItem] in
var items: [ContextMenuItem] = [
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
self?.navigationButtonAction(.openChatInfo(expandAvatar: true, recommendedChannels: false))
}))
]
if canViewStats {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChannelInfo_Stats, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer else {
return
}
strongSelf.view.endEditing(true)
let statsController: ViewController
if let channel = peer as? TelegramChannel, case .group = channel.info {
statsController = groupStatsController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id)
} else {
statsController = channelStatsController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id)
}
strongSelf.push(statsController)
})))
}
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_Search, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Search"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
self?.interfaceInteraction?.beginMessageSearch(.everything, "")
})))
return items
}
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
strongSelf.canReadHistory.set(false)
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
contextController.dismissed = { [weak self] in
self?.canReadHistory.set(true)
}
strongSelf.presentInGlobalOverlay(contextController)
}
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
self.avatarNode = avatarNode
case .customChatContents:
chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
chatInfoButtonItem.target = self
chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction)
self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo(expandAvatar: true, recommendedChannels: false), buttonItem: chatInfoButtonItem)
self.moreBarButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: self.presentationData.theme.rootController.navigationBar.buttonColor)))
self.moreInfoNavigationButton = ChatNavigationButton(action: .toggleInfoPanel, buttonItem: UIBarButtonItem(customDisplayNode: self.moreBarButton)!)
self.moreBarButton.contextAction = { [weak self] sourceNode, gesture in
guard let self else {
return
}
guard case let .peer(peerId) = self.chatLocation else {
return
}
if peerId == self.context.account.peerId {
PeerInfoScreenImpl.openSavedMessagesMoreMenu(context: self.context, sourceController: self, isViewingAsTopics: false, sourceView: sourceNode.view, gesture: gesture)
} else {
ChatListControllerImpl.openMoreMenu(context: self.context, peerId: peerId, sourceController: self, isViewingAsTopics: false, sourceView: sourceNode.view, gesture: gesture)
}
}
self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside)
self.navigationItem.titleView = self.chatTitleView
self.chatTitleView?.pressed = { [weak self] in
self?.navigationButtonAction(.openChatInfo(expandAvatar: false, recommendedChannels: false))
}
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
if let botStart = botStart, case .interactive = botStart.behavior {
return state.updatedBotStartPayload(botStart.payload)
} else {
return state
}
})
let chatLocationPeerId: PeerId? = chatLocation.peerId
self.accountPeerDisposable = (context.account.postbox.peerView(id: context.account.peerId)
|> deliverOnMainQueue).startStrict(next: { [weak self] peerView in
if let strongSelf = self {
let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
return state.updatedIsPremium(isPremium)
})
}
})
if let chatPeerId = chatLocation.peerId {
self.nameColorDisposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId)
)
|> deliverOnMainQueue).start(next: { [weak self] accountPeer, chatPeer in
guard let self, let accountPeer, let chatPeer else {
return
}
var nameColor: PeerNameColor?
if case let .channel(channel) = chatPeer, case .broadcast = channel.info {
nameColor = chatPeer.nameColor
} else {
nameColor = accountPeer.nameColor
}
var accountPeerColor: ChatPresentationInterfaceState.AccountPeerColor?
if let nameColor {
let colors = self.context.peerNameColors.get(nameColor)
var style: ChatPresentationInterfaceState.AccountPeerColor.Style = .solid
if colors.tertiary != nil {
style = .tripleDashed
} else if colors.secondary != nil {
style = .doubleDashed
}
accountPeerColor = ChatPresentationInterfaceState.AccountPeerColor(style: style)
}
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
return state.updatedAccountPeerColor(accountPeerColor)
})
})
}
let managingBot: Signal<ChatManagingBot?, NoError>
if let peerId = self.chatLocation.peerId, peerId.namespace == Namespaces.Peer.CloudUser {
managingBot = self.context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.ChatManagingBot(id: peerId)
)
|> mapToSignal { result -> Signal<ChatManagingBot?, NoError> in
guard let result else {
return .single(nil)
}
return context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: result.id)
)
|> map { botPeer -> ChatManagingBot? in
guard let botPeer else {
return nil
}
return ChatManagingBot(bot: botPeer, isPaused: result.isPaused, canReply: result.canReply, settingsUrl: result.manageUrl)
}
}
|> distinctUntilChanged
} else {
managingBot = .single(nil)
}
do {
let peerId = chatLocationPeerId
if case let .peer(peerView) = self.chatLocationInfoData, let peerId = peerId {
peerView.set(context.account.viewTracker.peerView(peerId))
var onlineMemberCount: Signal<Int32?, NoError> = .single(nil)
var hasScheduledMessages: Signal<Bool, NoError> = .single(false)
if peerId.namespace == Namespaces.Peer.CloudChannel {
let recentOnlineSignal: Signal<Int32?, NoError> = peerView.get()
|> map { view -> Bool? in
if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel {
if case .broadcast = peer.info {
return nil
} else if let memberCount = cachedData.participantsSummary.memberCount, memberCount > 50 {
return true
} else {
return false
}
} else {
return false
}
}
|> distinctUntilChanged
|> mapToSignal { isLarge -> Signal<Int32?, NoError> in
if let isLarge = isLarge {
if isLarge {
return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId)
|> map(Optional.init)
} else {
return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId)
|> map(Optional.init)
}
} else {
return .single(nil)
}
}
onlineMemberCount = recentOnlineSignal
self.reportIrrelvantGeoNoticePromise.set(context.engine.data.get(TelegramEngine.EngineData.Item.Notices.Notice(key: ApplicationSpecificNotice.irrelevantPeerGeoReportKey(peerId: peerId)))
|> map { entry -> Bool? in
if let _ = entry?.get(ApplicationSpecificBoolNotice.self) {
return true
} else {
return false
}
})
} else {
self.reportIrrelvantGeoNoticePromise.set(.single(nil))
}
var isScheduledOrPinnedMessages = false
switch subject {
case .scheduledMessages, .pinnedMessages, .messageOptions:
isScheduledOrPinnedMessages = true
default:
break
}
if chatLocation.peerId != nil, !isScheduledOrPinnedMessages, peerId.namespace != Namespaces.Peer.SecretChat {
let chatLocationContextHolder = self.chatLocationContextHolder
hasScheduledMessages = peerView.get()
|> take(1)
|> mapToSignal { view -> Signal<Bool, NoError> in
if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendSomething) {
return .single(false)
} else {
return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder))
|> map { view, _, _ in
return !view.entries.isEmpty
}
}
}
}
var displayedCountSignal: Signal<Int?, NoError> = .single(nil)
var subtitleTextSignal: Signal<String?, NoError> = .single(nil)
if case .pinnedMessages = subject {
displayedCountSignal = self.topPinnedMessageSignal(latest: true)
|> map { message -> Int? in
return message?.totalCount
}
|> distinctUntilChanged
} else if case let .messageOptions(peerIds, messageIds, info) = subject {
displayedCountSignal = self.presentationInterfaceStatePromise.get()
|> map { state -> Int? in
if let selectionState = state.interfaceState.selectionState {
return selectionState.selectedIds.count
} else {
return messageIds.count
}
}
|> distinctUntilChanged
let peers = self.context.account.postbox.multiplePeersView(peerIds)
|> take(1)
let presentationData = self.presentationData
switch info {
case let .forward(forward):
subtitleTextSignal = combineLatest(peers, forward.options, displayedCountSignal)
|> map { peersView, options, count in
let peers = peersView.peers.values
if !peers.isEmpty {
if peers.count == 1, let peer = peers.first {
if let peer = peer as? TelegramUser {
let displayName = EnginePeer(peer).compactDisplayTitle
if count == 1 {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardHidden(displayName).string
} else {
return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardVisible(displayName).string
}
} else {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardHidden(displayName).string
} else {
return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardVisible(displayName).string
}
}
} else if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if count == 1 {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardVisible
}
} else {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardVisible
}
}
} else {
if count == 1 {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardVisible
}
} else {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardVisible
}
}
}
} else {
if count == 1 {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardVisible
}
} else {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardVisible
}
}
}
} else {
return nil
}
}
case let .reply(reply):
subtitleTextSignal = reply.selectionState.get()
|> map { selectionState -> String? in
if !selectionState.canQuote {
return nil
}
return presentationData.strings.Chat_SubtitleQuoteSelectionTip
}
case let .link(link):
subtitleTextSignal = link.options
|> map { options -> String? in
if options.hasAlternativeLinks {
return presentationData.strings.Chat_SubtitleLinkListTip
} else {
return nil
}
}
|> distinctUntilChanged
}
}
let hasPeerInfo: Signal<Bool, NoError>
if peerId == context.account.peerId {
hasPeerInfo = .single(true)
|> then(
hasAvailablePeerInfoMediaPanes(context: context, peerId: peerId)
)
} else {
hasPeerInfo = .single(true)
}
enum MessageOptionsTitleInfo {
case reply(hasQuote: Bool)
}
let messageOptionsTitleInfo: Signal<MessageOptionsTitleInfo?, NoError>
if case let .messageOptions(_, _, info) = self.subject {
switch info {
case .forward, .link:
messageOptionsTitleInfo = .single(nil)
case let .reply(reply):
messageOptionsTitleInfo = reply.selectionState.get()
|> map { selectionState -> Bool in
return selectionState.quote != nil
}
|> distinctUntilChanged
|> map { hasQuote -> MessageOptionsTitleInfo in
return .reply(hasQuote: hasQuote)
}
}
} else {
messageOptionsTitleInfo = .single(nil)
}
self.titleDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, displayedCountSignal, subtitleTextSignal, self.presentationInterfaceStatePromise.get(), hasPeerInfo, messageOptionsTitleInfo)
|> deliverOnMainQueue).startStrict(next: { [weak self] peerView, onlineMemberCount, displayedCount, subtitleText, presentationInterfaceState, hasPeerInfo, messageOptionsTitleInfo in
if let strongSelf = self {
var isScheduledMessages = false
if case .scheduledMessages = presentationInterfaceState.subject {
isScheduledMessages = true
}
if case let .messageOptions(_, _, info) = presentationInterfaceState.subject {
if case .reply = info {
let titleContent: ChatTitleContent
if case let .reply(hasQuote) = messageOptionsTitleInfo, hasQuote {
titleContent = .custom(presentationInterfaceState.strings.Chat_TitleQuoteSelection, subtitleText, false)
} else {
titleContent = .custom(presentationInterfaceState.strings.Chat_TitleReply, subtitleText, false)
}
if strongSelf.chatTitleView?.titleContent != titleContent {
if strongSelf.chatTitleView?.titleContent != nil {
strongSelf.chatTitleView?.animateLayoutTransition()
}
strongSelf.chatTitleView?.titleContent = titleContent
}
} else if case .link = info {
strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitleLinkOptions, subtitleText, false)
} else if displayedCount == 1 {
strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitleSingle, subtitleText, false)
} else {
strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitle(Int32(displayedCount ?? 1)), subtitleText, false)
}
} else if let selectionState = presentationInterfaceState.interfaceState.selectionState {
if selectionState.selectedIds.count > 0 {
strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_SelectedMessages(Int32(selectionState.selectedIds.count)), nil, false)
} else {
if let reportReason = presentationInterfaceState.reportReason {
let title: String
switch reportReason {
case .spam:
title = presentationInterfaceState.strings.ReportPeer_ReasonSpam
case .fake:
title = presentationInterfaceState.strings.ReportPeer_ReasonFake
case .violence:
title = presentationInterfaceState.strings.ReportPeer_ReasonViolence
case .porno:
title = presentationInterfaceState.strings.ReportPeer_ReasonPornography
case .childAbuse:
title = presentationInterfaceState.strings.ReportPeer_ReasonChildAbuse
case .copyright:
title = presentationInterfaceState.strings.ReportPeer_ReasonCopyright
case .illegalDrugs:
title = presentationInterfaceState.strings.ReportPeer_ReasonIllegalDrugs
case .personalDetails:
title = presentationInterfaceState.strings.ReportPeer_ReasonPersonalDetails
case .custom:
title = presentationInterfaceState.strings.ReportPeer_ReasonOther
case .irrelevantLocation:
title = ""
}
strongSelf.chatTitleView?.titleContent = .custom(title, presentationInterfaceState.strings.Conversation_SelectMessages, false)
} else {
strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_SelectMessages, nil, false)
}
}
} else if let peer = peerViewMainPeer(peerView) {
if case .pinnedMessages = presentationInterfaceState.subject {
strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitlePinnedMessages(Int32(displayedCount ?? 1)), nil, false)
} else {
strongSelf.chatTitleView?.titleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo)
let imageOverride: AvatarNodeImageOverride?
if strongSelf.context.account.peerId == peer.id {
imageOverride = .savedMessagesIcon
} else if peer.id.isReplies {
imageOverride = .repliesIcon
} else if peer.id.isAnonymousSavedMessages {
imageOverride = .anonymousSavedMessagesIcon
} else if peer.isDeleted {
imageOverride = .deletedIcon
} else {
imageOverride = nil
}
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: EnginePeer(peer), overrideImage: imageOverride)
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = strongSelf.chatLocation.threadId == nil && peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil
strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = presentationInterfaceState.strings.Conversation_ContextMenuOpenProfile
strongSelf.storyStats = peerView.storyStats
if let avatarNode = strongSelf.avatarNode {
avatarNode.avatarNode.setStoryStats(storyStats: peerView.storyStats.flatMap { storyStats -> AvatarNode.StoryStats? in
if storyStats.totalCount == 0 {
return nil
}
if storyStats.unseenCount == 0 {
return nil
}
return AvatarNode.StoryStats(
totalCount: storyStats.totalCount,
unseenCount: storyStats.unseenCount,
hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends
)
}, presentationParams: AvatarNode.StoryPresentationParams(
colors: AvatarNode.Colors(theme: strongSelf.presentationData.theme),
lineWidth: 1.5,
inactiveLineWidth: 1.5
), transition: .immediate)
}
}
}
}
}))
let threadInfo: Signal<EngineMessageHistoryThread.Info?, NoError>
if let threadId = self.chatLocation.threadId {
let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: threadId)
threadInfo = context.account.postbox.combinedView(keys: [viewKey])
|> map { views -> EngineMessageHistoryThread.Info? in
guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else {
return nil
}
guard let data = view.info?.data.get(MessageHistoryThreadData.self) else {
return nil
}
return data.info
}
|> distinctUntilChanged
} else {
threadInfo = .single(nil)
}
let hasSearchTags: Signal<Bool, NoError>
if let peerId = self.chatLocation.peerId, peerId == context.account.peerId {
hasSearchTags = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Messages.SavedMessageTagStats(peerId: context.account.peerId, threadId: self.chatLocation.threadId)
)
|> map { tags -> Bool in
return !tags.isEmpty
}
|> distinctUntilChanged
} else {
hasSearchTags = .single(false)
}
let hasSavedChats: Signal<Bool, NoError>
if case .peer(context.account.peerId) = self.chatLocation {
hasSavedChats = context.engine.messages.savedMessagesHasPeersOtherThanSaved()
} else {
hasSavedChats = .single(false)
}
let isPremiumRequiredForMessaging: Signal<Bool, NoError>
if let peerId = self.chatLocation.peerId {
isPremiumRequiredForMessaging = context.engine.peers.subscribeIsPremiumRequiredForMessaging(id: peerId)
|> distinctUntilChanged
} else {
isPremiumRequiredForMessaging = .single(false)
}
self.peerDisposable.set(combineLatest(
queue: Queue.mainQueue(),
peerView.get(),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()),
onlineMemberCount,
hasScheduledMessages,
self.reportIrrelvantGeoNoticePromise.get(),
displayedCountSignal,
threadInfo,
hasSearchTags,
hasSavedChats,
isPremiumRequiredForMessaging,
managingBot
).startStrict(next: { [weak self] peerView, globalNotificationSettings, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot in
if let strongSelf = self {
if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo && strongSelf.presentationInterfaceState.hasSearchTags == hasSearchTags && strongSelf.presentationInterfaceState.hasSavedChats == hasSavedChats && strongSelf.presentationInterfaceState.isPremiumRequiredForMessaging == isPremiumRequiredForMessaging && managingBot == strongSelf.presentationInterfaceState.contactStatus?.managingBot {
return
}
strongSelf.reportIrrelvantGeoNotice = peerReportNotice
strongSelf.hasScheduledMessages = hasScheduledMessages
var upgradedToPeerId: PeerId?
var movedToForumTopics = false
if let previous = strongSelf.peerView, let group = previous.peers[previous.peerId] as? TelegramGroup, group.migrationReference == nil, let updatedGroup = peerView.peers[peerView.peerId] as? TelegramGroup, let migrationReference = updatedGroup.migrationReference {
upgradedToPeerId = migrationReference.peerId
}
if let previous = strongSelf.peerView, let channel = previous.peers[previous.peerId] as? TelegramChannel, !channel.flags.contains(.isForum), let updatedChannel = peerView.peers[peerView.peerId] as? TelegramChannel, updatedChannel.flags.contains(.isForum) {
movedToForumTopics = true
}
var shouldDismiss = false
if let previous = strongSelf.peerView, let group = previous.peers[previous.peerId] as? TelegramGroup, group.membership != .Removed, let updatedGroup = peerView.peers[peerView.peerId] as? TelegramGroup, updatedGroup.membership == .Removed {
shouldDismiss = true
} else if let previous = strongSelf.peerView, let channel = previous.peers[previous.peerId] as? TelegramChannel, channel.participationStatus != .kicked, let updatedChannel = peerView.peers[peerView.peerId] as? TelegramChannel, updatedChannel.participationStatus == .kicked {
shouldDismiss = true
} else if let previous = strongSelf.peerView, let secretChat = previous.peers[previous.peerId] as? TelegramSecretChat, case .active = secretChat.embeddedState, let updatedSecretChat = peerView.peers[peerView.peerId] as? TelegramSecretChat, case .terminated = updatedSecretChat.embeddedState {
shouldDismiss = true
}
var wasGroupChannel: Bool?
if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info {
if case .group = info {
wasGroupChannel = true
} else {
wasGroupChannel = false
}
}
var isGroupChannel: Bool?
if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info {
if case .group = info {
isGroupChannel = true
} else {
isGroupChannel = false
}
}
let firstTime = strongSelf.peerView == nil
strongSelf.peerView = peerView
strongSelf.threadInfo = threadInfo
if wasGroupChannel != isGroupChannel {
if let isGroupChannel = isGroupChannel, isGroupChannel {
let (recentDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.recent(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in })
let (adminsDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.admins(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in })
let disposable = DisposableSet()
disposable.add(recentDisposable)
disposable.add(adminsDisposable)
strongSelf.chatAdditionalDataDisposable.set(disposable)
} else {
strongSelf.chatAdditionalDataDisposable.set(nil)
}
}
if strongSelf.isNodeLoaded {
strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle
}
var peerIsMuted = false
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
peerIsMuted = true
} else if case .default = notificationSettings.muteState {
if let peer = peerView.peers[peerView.peerId] {
if peer is TelegramUser {
peerIsMuted = !globalNotificationSettings.privateChats.enabled
} else if peer is TelegramGroup {
peerIsMuted = !globalNotificationSettings.groupChats.enabled
} else if let channel = peer as? TelegramChannel {
switch channel.info {
case .group:
peerIsMuted = !globalNotificationSettings.groupChats.enabled
case .broadcast:
peerIsMuted = !globalNotificationSettings.channels.enabled
}
}
}
}
}
var peerDiscussionId: PeerId?
var peerGeoLocation: PeerGeoLocation?
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData {
if case .broadcast = peer.info {
if case let .known(value) = cachedData.linkedDiscussionPeerId {
peerDiscussionId = value
}
} else {
peerGeoLocation = cachedData.peerGeoLocation
}
}
var renderedPeer: RenderedPeer?
var contactStatus: ChatContactStatus?
var businessIntro: TelegramBusinessIntro?
if let peer = peerView.peers[peerView.peerId] {
if let cachedData = peerView.cachedData as? CachedUserData {
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot)
if case let .known(value) = cachedData.businessIntro {
businessIntro = value
}
} else if let cachedData = peerView.cachedData as? CachedGroupData {
var invitedBy: Peer?
if let invitedByPeerId = cachedData.invitedBy {
if let peer = peerView.peers[invitedByPeerId] {
invitedBy = peer
}
}
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot)
} else if let cachedData = peerView.cachedData as? CachedChannelData {
var canReportIrrelevantLocation = true
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.participationStatus == .member {
canReportIrrelevantLocation = false
}
if let peerReportNotice = peerReportNotice, peerReportNotice {
canReportIrrelevantLocation = false
}
var invitedBy: Peer?
if let invitedByPeerId = cachedData.invitedBy {
if let peer = peerView.peers[invitedByPeerId] {
invitedBy = peer
}
}
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot)
}
var peers = SimpleDictionary<PeerId, Peer>()
peers[peer.id] = peer
if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] {
peers[associatedPeer.id] = associatedPeer
}
renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media)
}
var isNotAccessible: Bool = false
if let cachedChannelData = peerView.cachedData as? CachedChannelData {
isNotAccessible = cachedChannelData.isNotAccessible
}
if firstTime && isNotAccessible {
strongSelf.context.account.viewTracker.forceUpdateCachedPeerData(peerId: peerView.peerId)
}
var hasBots: Bool = false
var hasBotCommands: Bool = false
var botMenuButton: BotMenuButton = .commands
var currentSendAsPeerId: PeerId?
var autoremoveTimeout: Int32?
var copyProtectionEnabled: Bool = false
var hasBirthdayToday = false
if let peer = peerView.peers[peerView.peerId] {
copyProtectionEnabled = peer.isCopyProtectionEnabled
if let cachedGroupData = peerView.cachedData as? CachedGroupData {
if !cachedGroupData.botInfos.isEmpty {
hasBots = true
}
let botCommands = cachedGroupData.botInfos.reduce(into: [], { result, info in
result.append(contentsOf: info.botInfo.commands)
})
if !botCommands.isEmpty {
hasBotCommands = true
}
if case let .known(value) = cachedGroupData.autoremoveTimeout {
autoremoveTimeout = value?.effectiveValue
}
} else if let cachedChannelData = peerView.cachedData as? CachedChannelData {
currentSendAsPeerId = cachedChannelData.sendAsPeerId
if let channel = peer as? TelegramChannel, case .group = channel.info {
if !cachedChannelData.botInfos.isEmpty {
hasBots = true
}
let botCommands = cachedChannelData.botInfos.reduce(into: [], { result, info in
result.append(contentsOf: info.botInfo.commands)
})
if !botCommands.isEmpty {
hasBotCommands = true
}
}
if case let .known(value) = cachedChannelData.autoremoveTimeout {
autoremoveTimeout = value?.effectiveValue
}
} else if let cachedUserData = peerView.cachedData as? CachedUserData {
botMenuButton = cachedUserData.botInfo?.menuButton ?? .commands
if case let .known(value) = cachedUserData.autoremoveTimeout {
autoremoveTimeout = value?.effectiveValue
}
if let botInfo = cachedUserData.botInfo, !botInfo.commands.isEmpty {
hasBotCommands = true
}
if let birthday = cachedUserData.birthday {
let today = Calendar.current.dateComponents(Set([.day, .month]), from: Date())
if today.day == Int(birthday.day) && today.month == Int(birthday.month) {
hasBirthdayToday = true
}
}
}
}
let isArchived: Bool = peerView.groupId == Namespaces.PeerGroup.archive
var explicitelyCanPinMessages: Bool = false
if let cachedUserData = peerView.cachedData as? CachedUserData {
explicitelyCanPinMessages = cachedUserData.canPinMessages
} else if peerView.peerId == context.account.peerId {
explicitelyCanPinMessages = true
}
var animated = false
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState {
animated = true
}
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, let updated = renderedPeer?.peer as? TelegramChannel {
if peer.participationStatus != updated.participationStatus {
animated = true
}
}
var didDisplayActionsPanel = false
if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings {
if !peerStatusSettings.flags.isEmpty {
if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) {
didDisplayActionsPanel = true
} else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) {
didDisplayActionsPanel = true
} else if peerStatusSettings.contains(.canShareContact) {
didDisplayActionsPanel = true
} else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
didDisplayActionsPanel = true
} else if peerStatusSettings.contains(.suggestAddMembers) {
didDisplayActionsPanel = true
}
}
}
if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, contactStatus.managingBot != nil {
didDisplayActionsPanel = true
}
if strongSelf.presentationInterfaceState.search != nil && strongSelf.presentationInterfaceState.hasSearchTags {
didDisplayActionsPanel = true
}
var displayActionsPanel = false
if let contactStatus = contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings {
if !peerStatusSettings.flags.isEmpty {
if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.canShareContact) {
displayActionsPanel = true
} else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.suggestAddMembers) {
displayActionsPanel = true
}
}
}
if let contactStatus, contactStatus.managingBot != nil {
displayActionsPanel = true
}
if strongSelf.presentationInterfaceState.search != nil && hasSearchTags {
displayActionsPanel = true
}
if displayActionsPanel != didDisplayActionsPanel {
animated = true
}
if strongSelf.preloadHistoryPeerId != peerDiscussionId {
strongSelf.preloadHistoryPeerId = peerDiscussionId
if let peerDiscussionId = peerDiscussionId {
let combinedDisposable = DisposableSet()
strongSelf.preloadHistoryPeerIdDisposable.set(combinedDisposable)
combinedDisposable.add(strongSelf.context.account.viewTracker.polledChannel(peerId: peerDiscussionId).startStrict())
combinedDisposable.add(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: peerDiscussionId))
} else {
strongSelf.preloadHistoryPeerIdDisposable.set(nil)
}
}
var appliedBoosts: Int32?
var boostsToUnrestrict: Int32?
if let cachedChannelData = peerView.cachedData as? CachedChannelData {
appliedBoosts = cachedChannelData.appliedBoosts
boostsToUnrestrict = cachedChannelData.boostsToUnrestrict
}
strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, {
return $0.updatedPeer { _ in
return renderedPeer
}.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedHasBotCommands(hasBotCommands).updatedBotMenuButton(botMenuButton).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages)
.updatedAutoremoveTimeout(autoremoveTimeout)
.updatedCurrentSendAsPeerId(currentSendAsPeerId)
.updatedCopyProtectionEnabled(copyProtectionEnabled)
.updatedHasSearchTags(hasSearchTags)
.updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging)
.updatedHasSavedChats(hasSavedChats)
.updatedAppliedBoosts(appliedBoosts)
.updatedBoostsToUnrestrict(boostsToUnrestrict)
.updatedHasBirthdayToday(hasBirthdayToday)
.updatedBusinessIntro(businessIntro)
.updatedInterfaceState { interfaceState in
var interfaceState = interfaceState
if let channel = renderedPeer?.peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if channel.hasBannedPermission(.banSendInstantVideos) == nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if channel.hasBannedPermission(.banSendVoice) == nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
}
}
} else if let group = renderedPeer?.peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
} else if group.hasBannedPermission(.banSendVoice) {
if !group.hasBannedPermission(.banSendInstantVideos) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if !group.hasBannedPermission(.banSendVoice) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
}
}
}
return interfaceState
}
})
if case .standard(.default) = mode, let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info {
var isRegularChat = false
if let subject = subject {
if case .message = subject {
isRegularChat = true
}
} else {
isRegularChat = true
}
if strongSelf.nextChannelToReadDisposable == nil, let peerId = strongSelf.chatLocation.peerId, let customChatNavigationStack = strongSelf.customChatNavigationStack {
if let index = customChatNavigationStack.firstIndex(of: peerId), index != customChatNavigationStack.count - 1 {
let nextPeerId = customChatNavigationStack[index + 1]
strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(),
strongSelf.context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: nextPeerId)
),
ApplicationSpecificNotice.getNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager)
)
|> then(.complete() |> delay(1.0, queue: .mainQueue()))
|> restart).startStrict(next: { nextPeer, nextChatSuggestionTip in
guard let strongSelf = self else {
return
}
strongSelf.offerNextChannelToRead = true
strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in
return (peer: nextPeer, threadData: nil, unreadCount: 0, location: .same)
}
strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3
let nextPeerId = nextPeer?.id
if strongSelf.preloadNextChatPeerId != nextPeerId {
strongSelf.preloadNextChatPeerId = nextPeerId
if let nextPeerId = nextPeerId {
let combinedDisposable = DisposableSet()
strongSelf.preloadNextChatPeerIdDisposable.set(combinedDisposable)
combinedDisposable.add(strongSelf.context.account.viewTracker.polledChannel(peerId: nextPeerId).startStrict())
combinedDisposable.add(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: nextPeerId))
} else {
strongSelf.preloadNextChatPeerIdDisposable.set(nil)
}
}
strongSelf.updateNextChannelToReadVisibility()
})
}
} else if isRegularChat, strongSelf.nextChannelToReadDisposable == nil {
//TODO:loc optimize
let accountPeerId = strongSelf.context.account.peerId
strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(),
strongSelf.context.engine.peers.getNextUnreadChannel(peerId: channel.id, chatListFilterId: strongSelf.currentChatListFilter, getFilterPredicate: { data in
return chatListFilterPredicate(filter: data, accountPeerId: accountPeerId)
}),
ApplicationSpecificNotice.getNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager)
)
|> then(.complete() |> delay(1.0, queue: .mainQueue()))
|> restart).startStrict(next: { nextPeer, nextChatSuggestionTip in
guard let strongSelf = self else {
return
}
strongSelf.offerNextChannelToRead = true
strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in
return (peer: nextPeer.peer, threadData: nil, unreadCount: nextPeer.unreadCount, location: nextPeer.location)
}
strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3
let nextPeerId = nextPeer?.peer.id
if strongSelf.preloadNextChatPeerId != nextPeerId {
strongSelf.preloadNextChatPeerId = nextPeerId
if let nextPeerId = nextPeerId {
let combinedDisposable = DisposableSet()
strongSelf.preloadNextChatPeerIdDisposable.set(combinedDisposable)
combinedDisposable.add(strongSelf.context.account.viewTracker.polledChannel(peerId: nextPeerId).startStrict())
combinedDisposable.add(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: nextPeerId))
} else {
strongSelf.preloadNextChatPeerIdDisposable.set(nil)
}
}
strongSelf.updateNextChannelToReadVisibility()
})
}
}
if !strongSelf.didSetChatLocationInfoReady {
strongSelf.didSetChatLocationInfoReady = true
strongSelf._chatLocationInfoReady.set(.single(true))
}
strongSelf.updateReminderActivity()
if let upgradedToPeerId = upgradedToPeerId {
if let navigationController = strongSelf.effectiveNavigationController {
var viewControllers = navigationController.viewControllers
if let index = viewControllers.firstIndex(where: { $0 === strongSelf }) {
viewControllers[index] = ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: upgradedToPeerId))
navigationController.setViewControllers(viewControllers, animated: false)
}
}
} else if movedToForumTopics {
if let navigationController = strongSelf.effectiveNavigationController {
let chatListController = strongSelf.context.sharedContext.makeChatListController(context: strongSelf.context, location: .forum(peerId: peerView.peerId), controlsHistoryPreload: false, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: false)
navigationController.replaceController(strongSelf, with: chatListController, animated: true)
}
} else if shouldDismiss {
strongSelf.dismiss()
}
}
}))
if peerId == context.account.peerId {
self.preloadSavedMessagesChatsDisposable = context.engine.messages.savedMessagesPeerListHead().start()
}
} else if case let .replyThread(messagePromise) = self.chatLocationInfoData, let peerId = peerId {
self.reportIrrelvantGeoNoticePromise.set(.single(nil))
let replyThreadType: ChatTitleContent.ReplyThreadType
var replyThreadId: Int64?
switch chatLocation {
case .peer:
replyThreadType = .replies
case let .replyThread(replyThreadMessage):
if replyThreadMessage.peerId == context.account.peerId {
replyThreadId = replyThreadMessage.threadId
replyThreadType = .replies
} else {
replyThreadId = replyThreadMessage.threadId
if replyThreadMessage.isChannelPost {
replyThreadType = .comments
} else {
replyThreadType = .replies
}
}
case .customChatContents:
replyThreadType = .replies
}
let peerView = context.account.viewTracker.peerView(peerId)
let messageAndTopic = messagePromise.get()
|> mapToSignal { message -> Signal<(message: Message?, threadData: MessageHistoryThreadData?, messageCount: Int), NoError> in
guard let replyThreadId = replyThreadId else {
return .single((message, nil, 0))
}
let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: replyThreadId)
let countViewKey: PostboxViewKey = .historyTagSummaryView(tag: MessageTags(), peerId: peerId, threadId: replyThreadId, namespace: Namespaces.Message.Cloud, customTag: nil)
let localCountViewKey: PostboxViewKey = .historyTagSummaryView(tag: MessageTags(), peerId: peerId, threadId: replyThreadId, namespace: Namespaces.Message.Local, customTag: nil)
return context.account.postbox.combinedView(keys: [viewKey, countViewKey, localCountViewKey])
|> map { views -> (message: Message?, threadData: MessageHistoryThreadData?, messageCount: Int) in
guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else {
return (message, nil, 0)
}
var messageCount = 0
if let summaryView = views.views[countViewKey] as? MessageHistoryTagSummaryView, let count = summaryView.count {
if replyThreadId == 1 {
messageCount += Int(count)
} else {
messageCount += max(Int(count) - 1, 0)
}
}
if let summaryView = views.views[localCountViewKey] as? MessageHistoryTagSummaryView, let count = summaryView.count {
messageCount += Int(count)
}
return (message, view.info?.data.get(MessageHistoryThreadData.self), messageCount)
}
}
let savedMessagesPeerId: PeerId?
if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.peerId == context.account.peerId {
savedMessagesPeerId = PeerId(replyThreadMessage.threadId)
} else {
savedMessagesPeerId = nil
}
let savedMessagesPeer: Signal<(peer: EnginePeer?, messageCount: Int)?, NoError>
if let savedMessagesPeerId {
let threadPeerId = savedMessagesPeerId
let basicPeerKey: PostboxViewKey = .basicPeer(threadPeerId)
let countViewKey: PostboxViewKey = .historyTagSummaryView(tag: MessageTags(), peerId: peerId, threadId: savedMessagesPeerId.toInt64(), namespace: Namespaces.Message.Cloud, customTag: nil)
savedMessagesPeer = context.account.postbox.combinedView(keys: [basicPeerKey, countViewKey])
|> map { views -> (peer: EnginePeer?, messageCount: Int)? in
let peer = ((views.views[basicPeerKey] as? BasicPeerView)?.peer).flatMap(EnginePeer.init)
var messageCount = 0
if let summaryView = views.views[countViewKey] as? MessageHistoryTagSummaryView, let count = summaryView.count {
messageCount += Int(count)
}
return (peer, messageCount)
}
} else {
savedMessagesPeer = .single(nil)
}
var isScheduledOrPinnedMessages = false
switch subject {
case .scheduledMessages, .pinnedMessages, .messageOptions:
isScheduledOrPinnedMessages = true
default:
break
}
var hasScheduledMessages: Signal<Bool, NoError> = .single(false)
if chatLocation.peerId != nil, !isScheduledOrPinnedMessages, peerId.namespace != Namespaces.Peer.SecretChat {
let chatLocationContextHolder = self.chatLocationContextHolder
hasScheduledMessages = peerView
|> take(1)
|> mapToSignal { view -> Signal<Bool, NoError> in
if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendSomething) {
return .single(false)
} else {
if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId {
return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: .peer(id: context.account.peerId), contextHolder: Atomic(value: nil)))
|> map { view, _, _ in
return !view.entries.isEmpty
}
|> distinctUntilChanged
} else {
return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder))
|> map { view, _, _ in
return !view.entries.isEmpty
}
|> distinctUntilChanged
}
}
}
}
var onlineMemberCount: Signal<Int32?, NoError> = .single(nil)
if peerId.namespace == Namespaces.Peer.CloudChannel {
let recentOnlineSignal: Signal<Int32?, NoError> = peerView
|> map { view -> Bool? in
if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel {
if case .broadcast = peer.info {
return nil
} else if let memberCount = cachedData.participantsSummary.memberCount, memberCount > 50 {
return true
} else {
return false
}
} else {
return false
}
}
|> distinctUntilChanged
|> mapToSignal { isLarge -> Signal<Int32?, NoError> in
if let isLarge = isLarge {
if isLarge {
return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId)
|> map(Optional.init)
} else {
return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId)
|> map(Optional.init)
}
} else {
return .single(nil)
}
}
onlineMemberCount = recentOnlineSignal
}
let hasSearchTags: Signal<Bool, NoError>
if let peerId = self.chatLocation.peerId, peerId == context.account.peerId {
hasSearchTags = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Messages.SavedMessageTagStats(peerId: context.account.peerId, threadId: self.chatLocation.threadId)
)
|> map { tags -> Bool in
return !tags.isEmpty
}
|> distinctUntilChanged
} else {
hasSearchTags = .single(false)
}
let hasSavedChats: Signal<Bool, NoError>
if case .peer(context.account.peerId) = self.chatLocation {
hasSavedChats = context.engine.messages.savedMessagesHasPeersOtherThanSaved()
} else {
hasSavedChats = .single(false)
}
let isPremiumRequiredForMessaging: Signal<Bool, NoError>
if let peerId = self.chatLocation.peerId {
isPremiumRequiredForMessaging = context.engine.peers.subscribeIsPremiumRequiredForMessaging(id: peerId)
|> distinctUntilChanged
} else {
isPremiumRequiredForMessaging = .single(false)
}
self.titleDisposable.set(nil)
self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(),
peerView,
messageAndTopic,
savedMessagesPeer,
onlineMemberCount,
hasScheduledMessages,
hasSearchTags,
hasSavedChats,
isPremiumRequiredForMessaging,
managingBot
)
|> deliverOnMainQueue).startStrict(next: { [weak self] peerView, messageAndTopic, savedMessagesPeer, onlineMemberCount, hasScheduledMessages, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot in
if let strongSelf = self {
strongSelf.hasScheduledMessages = hasScheduledMessages
var renderedPeer: RenderedPeer?
var contactStatus: ChatContactStatus?
var copyProtectionEnabled: Bool = false
var businessIntro: TelegramBusinessIntro?
if let peer = peerView.peers[peerView.peerId] {
copyProtectionEnabled = peer.isCopyProtectionEnabled
if let cachedData = peerView.cachedData as? CachedUserData {
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot)
if case let .known(value) = cachedData.businessIntro {
businessIntro = value
}
} else if let cachedData = peerView.cachedData as? CachedGroupData {
var invitedBy: Peer?
if let invitedByPeerId = cachedData.invitedBy {
if let peer = peerView.peers[invitedByPeerId] {
invitedBy = peer
}
}
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot)
} else if let cachedData = peerView.cachedData as? CachedChannelData {
var canReportIrrelevantLocation = true
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.participationStatus == .member {
canReportIrrelevantLocation = false
}
canReportIrrelevantLocation = false
var invitedBy: Peer?
if let invitedByPeerId = cachedData.invitedBy {
if let peer = peerView.peers[invitedByPeerId] {
invitedBy = peer
}
}
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot)
}
var peers = SimpleDictionary<PeerId, Peer>()
peers[peer.id] = peer
if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] {
peers[associatedPeer.id] = associatedPeer
}
renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media)
}
if let savedMessagesPeerId {
let mappedPeerData = ChatTitleContent.PeerData(
peerId: savedMessagesPeerId,
peer: savedMessagesPeer?.peer?._asPeer(),
isContact: true,
isSavedMessages: true,
notificationSettings: nil,
peerPresences: [:],
cachedData: nil
)
strongSelf.chatTitleView?.titleContent = .peer(peerView: mappedPeerData, customTitle: nil, onlineMemberCount: nil, isScheduledMessages: false, isMuted: false, customMessageCount: savedMessagesPeer?.messageCount ?? 0, isEnabled: true)
strongSelf.peerView = peerView
let imageOverride: AvatarNodeImageOverride?
if strongSelf.context.account.peerId == savedMessagesPeerId {
imageOverride = .myNotesIcon
} else if savedMessagesPeerId.isReplies {
imageOverride = .repliesIcon
} else if savedMessagesPeerId.isAnonymousSavedMessages {
imageOverride = .anonymousSavedMessagesIcon
} else if let peer = savedMessagesPeer?.peer, peer.isDeleted {
imageOverride = .deletedIcon
} else {
imageOverride = nil
}
if strongSelf.isNodeLoaded {
strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle
}
let animated = false
strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, {
return $0.updatedPeer { _ in
return renderedPeer
}.updatedSavedMessagesTopicPeer(savedMessagesPeer?.peer)
.updatedHasSearchTags(hasSearchTags)
.updatedHasSavedChats(hasSavedChats)
.updatedHasScheduledMessages(hasScheduledMessages)
})
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: savedMessagesPeer?.peer, overrideImage: imageOverride)
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false
strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = strongSelf.presentationData.strings.Conversation_ContextMenuOpenProfile
} else {
let message = messageAndTopic.message
var count = 0
if let message = message {
for attribute in message.attributes {
if let attribute = attribute as? ReplyThreadMessageAttribute {
count = Int(attribute.count)
break
}
}
}
var peerIsMuted = false
if let threadData = messageAndTopic.threadData {
if case let .muted(until) = threadData.notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
peerIsMuted = true
}
} else if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
peerIsMuted = true
}
}
if let threadInfo = messageAndTopic.threadData?.info {
strongSelf.chatTitleView?.titleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: threadInfo.title, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: peerIsMuted, customMessageCount: messageAndTopic.messageCount == 0 ? nil : messageAndTopic.messageCount, isEnabled: true)
let avatarContent: EmojiStatusComponent.Content
if strongSelf.chatLocation.threadId == 1 {
avatarContent = .image(image: PresentationResourcesChat.chatGeneralThreadIcon(strongSelf.presentationData.theme))
} else if let fileId = threadInfo.icon {
avatarContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: strongSelf.presentationData.theme.list.mediaPlaceholderColor, themeColor: strongSelf.presentationData.theme.list.itemAccentColor, loopMode: .count(1))
} else {
avatarContent = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: CGSize(width: 32.0, height: 32.0))
}
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setStatus(context: strongSelf.context, content: avatarContent)
} else {
strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, count: count)
}
var wasGroupChannel: Bool?
if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info {
if case .group = info {
wasGroupChannel = true
} else {
wasGroupChannel = false
}
}
var isGroupChannel: Bool?
if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info {
if case .group = info {
isGroupChannel = true
} else {
isGroupChannel = false
}
}
let firstTime = strongSelf.peerView == nil
if wasGroupChannel != isGroupChannel {
if let isGroupChannel = isGroupChannel, isGroupChannel {
let (recentDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.recent(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in })
let (adminsDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.admins(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in })
let disposable = DisposableSet()
disposable.add(recentDisposable)
disposable.add(adminsDisposable)
strongSelf.chatAdditionalDataDisposable.set(disposable)
} else {
strongSelf.chatAdditionalDataDisposable.set(nil)
}
}
strongSelf.peerView = peerView
strongSelf.threadInfo = messageAndTopic.threadData?.info
if strongSelf.isNodeLoaded {
strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle
}
var peerDiscussionId: PeerId?
var peerGeoLocation: PeerGeoLocation?
var currentSendAsPeerId: PeerId?
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData {
currentSendAsPeerId = cachedData.sendAsPeerId
if case .broadcast = peer.info {
if case let .known(value) = cachedData.linkedDiscussionPeerId {
peerDiscussionId = value
}
} else {
peerGeoLocation = cachedData.peerGeoLocation
}
}
var isNotAccessible: Bool = false
if let cachedChannelData = peerView.cachedData as? CachedChannelData {
isNotAccessible = cachedChannelData.isNotAccessible
}
if firstTime && isNotAccessible {
strongSelf.context.account.viewTracker.forceUpdateCachedPeerData(peerId: peerView.peerId)
}
var hasBots: Bool = false
if let peer = peerView.peers[peerView.peerId] {
if let cachedGroupData = peerView.cachedData as? CachedGroupData {
if !cachedGroupData.botInfos.isEmpty {
hasBots = true
}
} else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let channel = peer as? TelegramChannel, case .group = channel.info {
if !cachedChannelData.botInfos.isEmpty {
hasBots = true
}
}
}
let isArchived: Bool = peerView.groupId == Namespaces.PeerGroup.archive
var explicitelyCanPinMessages: Bool = false
if let cachedUserData = peerView.cachedData as? CachedUserData {
explicitelyCanPinMessages = cachedUserData.canPinMessages
} else if peerView.peerId == context.account.peerId {
explicitelyCanPinMessages = true
}
var animated = false
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState {
animated = true
}
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, let updated = renderedPeer?.peer as? TelegramChannel {
if peer.participationStatus != updated.participationStatus {
animated = true
}
}
var didDisplayActionsPanel = false
if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings {
if !peerStatusSettings.flags.isEmpty {
if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) {
didDisplayActionsPanel = true
} else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) {
didDisplayActionsPanel = true
} else if peerStatusSettings.contains(.canShareContact) {
didDisplayActionsPanel = true
} else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
didDisplayActionsPanel = true
} else if peerStatusSettings.contains(.suggestAddMembers) {
didDisplayActionsPanel = true
}
}
}
if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, contactStatus.managingBot != nil {
didDisplayActionsPanel = true
}
var displayActionsPanel = false
if let contactStatus = contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings {
if !peerStatusSettings.flags.isEmpty {
if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.canShareContact) {
displayActionsPanel = true
} else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.suggestAddMembers) {
displayActionsPanel = true
}
}
}
if let contactStatus, contactStatus.managingBot != nil {
displayActionsPanel = true
}
if displayActionsPanel != didDisplayActionsPanel {
animated = true
}
if strongSelf.preloadHistoryPeerId != peerDiscussionId {
strongSelf.preloadHistoryPeerId = peerDiscussionId
if let peerDiscussionId = peerDiscussionId {
strongSelf.preloadHistoryPeerIdDisposable.set(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: peerDiscussionId))
} else {
strongSelf.preloadHistoryPeerIdDisposable.set(nil)
}
}
var appliedBoosts: Int32?
var boostsToUnrestrict: Int32?
if let cachedChannelData = peerView.cachedData as? CachedChannelData {
appliedBoosts = cachedChannelData.appliedBoosts
boostsToUnrestrict = cachedChannelData.boostsToUnrestrict
}
strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, {
return $0.updatedPeer { _ in
return renderedPeer
}.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages).updatedCurrentSendAsPeerId(currentSendAsPeerId)
.updatedCopyProtectionEnabled(copyProtectionEnabled)
.updatedHasSearchTags(hasSearchTags)
.updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging)
.updatedHasSavedChats(hasSavedChats)
.updatedAppliedBoosts(appliedBoosts)
.updatedBoostsToUnrestrict(boostsToUnrestrict)
.updatedBusinessIntro(businessIntro)
.updatedInterfaceState { interfaceState in
var interfaceState = interfaceState
if let channel = renderedPeer?.peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if channel.hasBannedPermission(.banSendInstantVideos) == nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if channel.hasBannedPermission(.banSendVoice) == nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
}
}
} else if let group = renderedPeer?.peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
} else if group.hasBannedPermission(.banSendVoice) {
if !group.hasBannedPermission(.banSendInstantVideos) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if !group.hasBannedPermission(.banSendVoice) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
}
}
}
return interfaceState
}
})
if let replyThreadId, let channel = renderedPeer?.peer as? TelegramChannel, channel.isForum, strongSelf.nextChannelToReadDisposable == nil {
strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(),
strongSelf.context.engine.peers.getNextUnreadForumTopic(peerId: channel.id, topicId: Int32(clamping: replyThreadId)),
ApplicationSpecificNotice.getNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager)
)
|> then(.complete() |> delay(1.0, queue: .mainQueue()))
|> restart).startStrict(next: { nextThreadData, nextChatSuggestionTip in
guard let strongSelf = self else {
return
}
strongSelf.offerNextChannelToRead = true
strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextThreadData.flatMap { nextThreadData -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in
return (peer: EnginePeer(channel), threadData: nextThreadData, unreadCount: Int(nextThreadData.data.incomingUnreadCount), location: .same)
}
strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3
strongSelf.updateNextChannelToReadVisibility()
})
}
}
if !strongSelf.didSetChatLocationInfoReady {
strongSelf.didSetChatLocationInfoReady = true
strongSelf._chatLocationInfoReady.set(.single(true))
}
}
}))
} else if case .customChatContents = self.chatLocationInfoData {
self.reportIrrelvantGeoNoticePromise.set(.single(nil))
self.titleDisposable.set(nil)
if case let .customChatContents(customChatContents) = self.subject {
switch customChatContents.kind {
case .hashTagSearch:
break
case let .quickReplyMessageInput(shortcut, shortcutType):
switch shortcutType {
case .generic:
self.chatTitleView?.titleContent = .custom("\(shortcut)", nil, false)
case .greeting:
self.chatTitleView?.titleContent = .custom(self.presentationData.strings.QuickReply_TitleGreetingMessage, nil, false)
case .away:
self.chatTitleView?.titleContent = .custom(self.presentationData.strings.QuickReply_TitleAwayMessage, nil, false)
}
case let .businessLinkSetup(link):
let linkUrl: String
if link.url.hasPrefix("https://") {
linkUrl = String(link.url[link.url.index(link.url.startIndex, offsetBy: "https://".count)...])
} else {
linkUrl = link.url
}
self.chatTitleView?.titleContent = .custom(link.title ?? self.presentationData.strings.Business_Links_EditLinkTitle, linkUrl, false)
}
} else {
self.chatTitleView?.titleContent = .custom(" ", nil, false)
}
if !self.didSetChatLocationInfoReady {
self.didSetChatLocationInfoReady = true
self._chatLocationInfoReady.set(.single(true))
}
}
}
self.botCallbackAlertMessageDisposable = (self.botCallbackAlertMessage.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] message in
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
return $0.updatedTitlePanelContext {
if let message = message {
if let index = $0.firstIndex(where: {
switch $0 {
case .toastAlert:
return true
default:
return false
}
}) {
if $0[index] != ChatTitlePanelContext.toastAlert(message) {
var updatedContexts = $0
updatedContexts[index] = .toastAlert(message)
return updatedContexts
} else {
return $0
}
} else {
var updatedContexts = $0
updatedContexts.append(.toastAlert(message))
return updatedContexts.sorted()
}
} else {
if let index = $0.firstIndex(where: {
switch $0 {
case .toastAlert:
return true
default:
return false
}
}) {
var updatedContexts = $0
updatedContexts.remove(at: index)
return updatedContexts
} else {
return $0
}
}
}
})
}
})
self.audioRecorderDisposable = (self.audioRecorder.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] audioRecorder in
if let strongSelf = self {
if strongSelf.audioRecorderValue !== audioRecorder {
strongSelf.audioRecorderValue = audioRecorder
strongSelf.lockOrientation = audioRecorder != nil
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
$0.updatedInputTextPanelState { panelState in
let isLocked = strongSelf.lockMediaRecordingRequestId == strongSelf.beginMediaRecordingRequestId
if let audioRecorder = audioRecorder {
if panelState.mediaRecordingState == nil {
return panelState.withUpdatedMediaRecordingState(.audio(recorder: audioRecorder, isLocked: isLocked))
}
} else {
if case .waitingForPreview = panelState.mediaRecordingState {
return panelState
}
return panelState.withUpdatedMediaRecordingState(nil)
}
return panelState
}
})
strongSelf.audioRecorderStatusDisposable?.dispose()
if let audioRecorder = audioRecorder {
if !audioRecorder.beginWithTone {
strongSelf.recorderFeedback?.impact(.light)
}
audioRecorder.start()
strongSelf.audioRecorderStatusDisposable = (audioRecorder.recordingState
|> deliverOnMainQueue).startStrict(next: { value in
if case .stopped = value {
self?.stopMediaRecorder()
}
})
} else {
strongSelf.audioRecorderStatusDisposable = nil
}
strongSelf.updateDownButtonVisibility()
}
}
})
self.videoRecorderDisposable = (self.videoRecorder.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] videoRecorder in
if let strongSelf = self {
if strongSelf.videoRecorderValue !== videoRecorder {
let previousVideoRecorderValue = strongSelf.videoRecorderValue
strongSelf.videoRecorderValue = videoRecorder
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
$0.updatedInputTextPanelState { panelState in
if let videoRecorder = videoRecorder {
if panelState.mediaRecordingState == nil {
let recordingStatus = videoRecorder.recordingStatus
return panelState.withUpdatedMediaRecordingState(.video(status: .recording(InstantVideoControllerRecordingStatus(micLevel: recordingStatus.micLevel, duration: recordingStatus.duration)), isLocked: strongSelf.lockMediaRecordingRequestId == strongSelf.beginMediaRecordingRequestId))
}
} else {
return panelState.withUpdatedMediaRecordingState(nil)
}
return panelState
}
})
if let videoRecorder = videoRecorder {
strongSelf.recorderFeedback?.impact(.light)
videoRecorder.onStop = {
if let strongSelf = self {
strongSelf.dismissMediaRecorder(.pause)
}
}
strongSelf.present(videoRecorder, in: .window(.root))
if strongSelf.lockMediaRecordingRequestId == strongSelf.beginMediaRecordingRequestId {
videoRecorder.lockVideoRecording()
}
}
strongSelf.updateDownButtonVisibility()
if let previousVideoRecorderValue = previousVideoRecorderValue {
previousVideoRecorderValue.discardVideo()
}
}
}
})
if let botStart = botStart, case .automatic = botStart.behavior {
self.startBot(botStart.payload)
}
let activitySpace: PeerActivitySpace?
switch self.chatLocation {
case let .peer(peerId):
activitySpace = PeerActivitySpace(peerId: peerId, category: .global)
case let .replyThread(replyThreadMessage):
activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId))
case .customChatContents:
activitySpace = nil
}
if let activitySpace = activitySpace {
self.inputActivityDisposable = (self.typingActivityPromise.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
if let strongSelf = self, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil && strongSelf.presentationInterfaceState.subject != .scheduledMessages && strongSelf.presentationInterfaceState.currentSendAsPeerId == nil {
strongSelf.context.account.updateLocalInputActivity(peerId: activitySpace, activity: .typingText, isPresent: value)
}
})
self.choosingStickerActivityDisposable = (self.choosingStickerActivityPromise.get()
|> mapToSignal { value -> Signal<Bool, NoError> in
if value {
return .single(true)
} else {
return .single(false) |> delay(2.0, queue: Queue.mainQueue())
}
}
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
if let strongSelf = self, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil && strongSelf.presentationInterfaceState.subject != .scheduledMessages && strongSelf.presentationInterfaceState.currentSendAsPeerId == nil {
if value {
strongSelf.context.account.updateLocalInputActivity(peerId: activitySpace, activity: .typingText, isPresent: false)
}
strongSelf.context.account.updateLocalInputActivity(peerId: activitySpace, activity: .choosingSticker, isPresent: value)
}
})
self.recordingActivityDisposable = (self.recordingActivityPromise.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
if let strongSelf = self, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil && strongSelf.presentationInterfaceState.subject != .scheduledMessages && strongSelf.presentationInterfaceState.currentSendAsPeerId == nil {
strongSelf.acquiredRecordingActivityDisposable?.dispose()
switch value {
case .voice:
strongSelf.acquiredRecordingActivityDisposable = strongSelf.context.account.acquireLocalInputActivity(peerId: activitySpace, activity: .recordingVoice)
case .instantVideo:
strongSelf.acquiredRecordingActivityDisposable = strongSelf.context.account.acquireLocalInputActivity(peerId: activitySpace, activity: .recordingInstantVideo)
case .none:
strongSelf.acquiredRecordingActivityDisposable = nil
}
}
})
}
let themeEmoticon: Signal<String?, NoError> = self.chatThemeEmoticonPromise.get()
|> distinctUntilChanged
let uploadingChatWallpaper: Signal<TelegramWallpaper?, NoError>
if let peerId = self.chatLocation.peerId {
uploadingChatWallpaper = self.context.account.pendingPeerMediaUploadManager.uploadingPeerMedia
|> map { uploadingPeerMedia -> TelegramWallpaper? in
if let item = uploadingPeerMedia[peerId], case let .wallpaper(wallpaper, _) = item.content {
return wallpaper
} else {
return nil
}
}
|> distinctUntilChanged
} else {
uploadingChatWallpaper = .single(nil)
}
let chatWallpaper: Signal<TelegramWallpaper?, NoError> = combineLatest(self.chatWallpaperPromise.get(), uploadingChatWallpaper)
|> map { chatWallpaper, uploadingChatWallpaper in
return uploadingChatWallpaper ?? chatWallpaper
}
|> distinctUntilChanged
let themeSettings = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings])
|> map { sharedData -> PresentationThemeSettings in
let themeSettings: PresentationThemeSettings
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) {
themeSettings = current
} else {
themeSettings = PresentationThemeSettings.defaultSettings
}
return themeSettings
}
let accountManager = context.sharedContext.accountManager
let currentThemeEmoticon = Atomic<(String?, Bool)?>(value: nil)
self.presentationDataDisposable = combineLatest(
queue: Queue.mainQueue(),
context.sharedContext.presentationData,
themeSettings,
context.engine.themes.getChatThemes(accountManager: accountManager, onlyCached: true),
themeEmoticon,
self.themeEmoticonAndDarkAppearancePreviewPromise.get(),
chatWallpaper
).startStrict(next: { [weak self] presentationData, themeSettings, chatThemes, themeEmoticon, themeEmoticonAndDarkAppearance, chatWallpaper in
if let strongSelf = self {
let (themeEmoticonPreview, darkAppearancePreview) = themeEmoticonAndDarkAppearance
var chatWallpaper = chatWallpaper
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
let previousChatWallpaper = strongSelf.presentationData.chatWallpaper
var themeEmoticon = themeEmoticon
if let themeEmoticonPreview = themeEmoticonPreview {
if !themeEmoticonPreview.isEmpty {
if themeEmoticon?.strippedEmoji != themeEmoticonPreview.strippedEmoji {
chatWallpaper = nil
themeEmoticon = themeEmoticonPreview
}
} else {
themeEmoticon = nil
}
}
if strongSelf.chatLocation.peerId == strongSelf.context.account.peerId {
themeEmoticon = nil
}
var presentationData = presentationData
var useDarkAppearance = presentationData.theme.overallDarkAppearance
if let forcedTheme = strongSelf.forcedTheme {
presentationData = presentationData.withUpdated(theme: forcedTheme)
} else {
if let wallpaper = chatWallpaper, case let .emoticon(wallpaperEmoticon) = wallpaper, let theme = chatThemes.first(where: { $0.emoticon?.strippedEmoji == wallpaperEmoticon.strippedEmoji }) {
let themeSettings: TelegramThemeSettings?
if let matching = theme.settings?.first(where: { $0.baseTheme == presentationData.theme.referenceTheme.baseTheme }) {
themeSettings = matching
} else {
themeSettings = theme.settings?.first
}
if let themeWallpaper = themeSettings?.wallpaper {
chatWallpaper = themeWallpaper
}
}
if let themeEmoticon = themeEmoticon, let theme = chatThemes.first(where: { $0.emoticon?.strippedEmoji == themeEmoticon.strippedEmoji }) {
if let darkAppearancePreview = darkAppearancePreview {
useDarkAppearance = darkAppearancePreview
}
if let theme = makePresentationTheme(cloudTheme: theme, dark: useDarkAppearance) {
theme.forceSync = true
presentationData = presentationData.withUpdated(theme: theme).withUpdated(chatWallpaper: theme.chat.defaultWallpaper)
Queue.mainQueue().after(1.0, {
theme.forceSync = false
})
}
} else if let darkAppearancePreview = darkAppearancePreview {
useDarkAppearance = darkAppearancePreview
let lightTheme: PresentationTheme
let lightWallpaper: TelegramWallpaper
let darkTheme: PresentationTheme
let darkWallpaper: TelegramWallpaper
if presentationData.autoNightModeTriggered {
darkTheme = presentationData.theme
darkWallpaper = presentationData.chatWallpaper
var currentColors = themeSettings.themeSpecificAccentColors[themeSettings.theme.index]
if let colors = currentColors, colors.baseColor == .theme {
currentColors = nil
}
let themeSpecificWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: themeSettings.theme, accentColor: currentColors)] ?? themeSettings.themeSpecificChatWallpapers[themeSettings.theme.index])
if let themeSpecificWallpaper = themeSpecificWallpaper {
lightWallpaper = themeSpecificWallpaper
} else {
let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: themeSettings.theme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors ?? [], wallpaper: currentColors?.wallpaper, baseColor: currentColors?.baseColor, preview: true) ?? defaultPresentationTheme
lightWallpaper = theme.chat.defaultWallpaper
}
var preferredBaseTheme: TelegramBaseTheme?
if let baseTheme = themeSettings.themePreferredBaseTheme[themeSettings.theme.index], [.classic, .day].contains(baseTheme) {
preferredBaseTheme = baseTheme
}
lightTheme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: themeSettings.theme, baseTheme: preferredBaseTheme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors ?? [], wallpaper: currentColors?.wallpaper, baseColor: currentColors?.baseColor, serviceBackgroundColor: defaultServiceBackgroundColor) ?? defaultPresentationTheme
} else {
lightTheme = presentationData.theme
lightWallpaper = presentationData.chatWallpaper
let automaticTheme = themeSettings.automaticThemeSwitchSetting.theme
let effectiveColors = themeSettings.themeSpecificAccentColors[automaticTheme.index]
let themeSpecificWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: automaticTheme, accentColor: effectiveColors)] ?? themeSettings.themeSpecificChatWallpapers[automaticTheme.index])
var preferredBaseTheme: TelegramBaseTheme?
if let baseTheme = themeSettings.themePreferredBaseTheme[automaticTheme.index], [.night, .tinted].contains(baseTheme) {
preferredBaseTheme = baseTheme
} else {
preferredBaseTheme = .night
}
darkTheme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: automaticTheme, baseTheme: preferredBaseTheme, accentColor: effectiveColors?.color, bubbleColors: effectiveColors?.customBubbleColors ?? [], wallpaper: effectiveColors?.wallpaper, baseColor: effectiveColors?.baseColor, serviceBackgroundColor: defaultServiceBackgroundColor) ?? defaultPresentationTheme
if let themeSpecificWallpaper = themeSpecificWallpaper {
darkWallpaper = themeSpecificWallpaper
} else {
switch lightWallpaper {
case .builtin, .color, .gradient:
darkWallpaper = darkTheme.chat.defaultWallpaper
case .file:
if lightWallpaper.isPattern {
darkWallpaper = darkTheme.chat.defaultWallpaper
} else {
darkWallpaper = lightWallpaper
}
default:
darkWallpaper = lightWallpaper
}
}
}
if darkAppearancePreview {
darkTheme.forceSync = true
Queue.mainQueue().after(1.0, {
darkTheme.forceSync = false
})
presentationData = presentationData.withUpdated(theme: darkTheme).withUpdated(chatWallpaper: darkWallpaper)
} else {
lightTheme.forceSync = true
Queue.mainQueue().after(1.0, {
lightTheme.forceSync = false
})
presentationData = presentationData.withUpdated(theme: lightTheme).withUpdated(chatWallpaper: lightWallpaper)
}
}
}
if let forcedWallpaper = strongSelf.forcedWallpaper {
presentationData = presentationData.withUpdated(chatWallpaper: forcedWallpaper)
} else if let chatWallpaper {
presentationData = presentationData.withUpdated(chatWallpaper: chatWallpaper)
}
let isFirstTime = !strongSelf.didSetPresentationData
strongSelf.presentationData = presentationData
strongSelf.didSetPresentationData = true
let previousThemeEmoticon = currentThemeEmoticon.swap((themeEmoticon, useDarkAppearance))
if isFirstTime || previousTheme != presentationData.theme || previousStrings !== presentationData.strings || presentationData.chatWallpaper != previousChatWallpaper {
strongSelf.themeAndStringsUpdated()
controllerInteraction.updatedPresentationData = strongSelf.updatedPresentationData
strongSelf.presentationDataPromise.set(.single(strongSelf.presentationData))
if !isFirstTime && (previousThemeEmoticon?.0 != themeEmoticon || previousThemeEmoticon?.1 != useDarkAppearance) {
strongSelf.presentCrossfadeSnapshot()
}
}
strongSelf.presentationReady.set(.single(true))
}
})
self.automaticMediaDownloadSettingsDisposable = (context.sharedContext.automaticMediaDownloadSettings
|> deliverOnMainQueue).startStrict(next: { [weak self] downloadSettings in
if let strongSelf = self, strongSelf.automaticMediaDownloadSettings != downloadSettings {
strongSelf.automaticMediaDownloadSettings = downloadSettings
strongSelf.controllerInteraction?.automaticMediaDownloadSettings = downloadSettings
if strongSelf.isNodeLoaded {
strongSelf.chatDisplayNode.updateAutomaticMediaDownloadSettings(downloadSettings)
}
}
})
self.stickerSettingsDisposable = combineLatest(queue: Queue.mainQueue(), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]), self.disableStickerAnimationsPromise.get()).startStrict(next: { [weak self] sharedData, disableStickerAnimations in
var stickerSettings = StickerSettings.defaultSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) {
stickerSettings = value
}
let chatStickerSettings = ChatInterfaceStickerSettings(stickerSettings: stickerSettings)
if let strongSelf = self, strongSelf.stickerSettings != chatStickerSettings || strongSelf.disableStickerAnimationsValue != disableStickerAnimations {
strongSelf.stickerSettings = chatStickerSettings
strongSelf.disableStickerAnimationsValue = disableStickerAnimations
strongSelf.controllerInteraction?.stickerSettings = chatStickerSettings
if strongSelf.isNodeLoaded {
strongSelf.chatDisplayNode.updateStickerSettings(chatStickerSettings, forceStopAnimations: disableStickerAnimations)
}
}
})
var wasInForeground = true
self.applicationInForegroundDisposable = (context.sharedContext.applicationBindings.applicationInForeground
|> distinctUntilChanged
|> deliverOn(Queue.mainQueue())).startStrict(next: { [weak self] value in
if let strongSelf = self, strongSelf.isNodeLoaded {
if !value {
strongSelf.saveInterfaceState()
strongSelf.raiseToListen?.applicationResignedActive()
strongSelf.stopMediaRecorder()
} else {
if !wasInForeground {
strongSelf.chatDisplayNode.recursivelyEnsureDisplaySynchronously(true)
}
}
wasInForeground = value
}
})
if case let .peer(peerId) = chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {
self.applicationInFocusDisposable = (context.sharedContext.applicationBindings.applicationIsActive
|> distinctUntilChanged
|> deliverOn(Queue.mainQueue())).startStrict(next: { [weak self] value in
guard let strongSelf = self, strongSelf.isNodeLoaded else {
return
}
strongSelf.chatDisplayNode.updateIsBlurred(!value)
})
}
self.canReadHistoryDisposable = (combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in
return a && b
} |> deliverOnMainQueue).startStrict(next: { [weak self] value in
if let strongSelf = self, strongSelf.canReadHistoryValue != value {
strongSelf.canReadHistoryValue = value
strongSelf.raiseToListen?.enabled = value
strongSelf.isReminderActivityEnabled = value
strongSelf.updateReminderActivity()
}
})
self.networkStateDisposable = (context.account.networkState |> deliverOnMainQueue).startStrict(next: { [weak self] state in
if let strongSelf = self, case .standard(.default) = strongSelf.presentationInterfaceState.mode {
strongSelf.chatTitleView?.networkState = state
}
})
if case let .messageOptions(_, messageIds, _) = self.subject, messageIds.count > 1 {
self.updateChatPresentationInterfaceState(interactive: false, { state in
return state.updatedInterfaceState({ $0.withUpdatedSelectedMessages(messageIds) })
})
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
let _ = ChatControllerCount.modify { value in
return value - 1
}
let deallocate: () -> Void = {
self.historyStateDisposable?.dispose()
self.messageIndexDisposable.dispose()
self.navigationActionDisposable.dispose()
self.galleryHiddenMesageAndMediaDisposable.dispose()
self.temporaryHiddenGalleryMediaDisposable.dispose()
self.peerDisposable.dispose()
self.accountPeerDisposable?.dispose()
self.titleDisposable.dispose()
self.messageContextDisposable.dispose()
self.controllerNavigationDisposable.dispose()
self.sentMessageEventsDisposable.dispose()
self.failedMessageEventsDisposable.dispose()
self.sentPeerMediaMessageEventsDisposable.dispose()
self.messageActionCallbackDisposable.dispose()
self.messageActionUrlAuthDisposable.dispose()
self.editMessageDisposable.dispose()
self.editMessageErrorsDisposable.dispose()
self.enqueueMediaMessageDisposable.dispose()
self.resolvePeerByNameDisposable?.dispose()
self.shareStatusDisposable?.dispose()
self.clearCacheDisposable?.dispose()
self.bankCardDisposable?.dispose()
self.botCallbackAlertMessageDisposable?.dispose()
self.selectMessagePollOptionDisposables?.dispose()
for (_, info) in self.contextQueryStates {
info.1.dispose()
}
self.urlPreviewQueryState?.1.dispose()
self.editingUrlPreviewQueryState?.1.dispose()
self.replyMessageState?.1.dispose()
self.audioRecorderDisposable?.dispose()
self.audioRecorderStatusDisposable?.dispose()
self.videoRecorderDisposable?.dispose()
self.buttonKeyboardMessageDisposable?.dispose()
self.cachedDataDisposable?.dispose()
self.resolveUrlDisposable?.dispose()
self.chatUnreadCountDisposable?.dispose()
self.buttonUnreadCountDisposable?.dispose()
self.chatUnreadMentionCountDisposable?.dispose()
self.peerInputActivitiesDisposable?.dispose()
self.interactiveEmojiSyncDisposable.dispose()
self.recentlyUsedInlineBotsDisposable?.dispose()
self.unpinMessageDisposable?.dispose()
self.inputActivityDisposable?.dispose()
self.recordingActivityDisposable?.dispose()
self.acquiredRecordingActivityDisposable?.dispose()
self.presentationDataDisposable?.dispose()
self.searchDisposable?.dispose()
self.applicationInForegroundDisposable?.dispose()
self.applicationInFocusDisposable?.dispose()
self.canReadHistoryDisposable?.dispose()
self.networkStateDisposable?.dispose()
self.chatAdditionalDataDisposable.dispose()
self.shareStatusDisposable?.dispose()
self.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeTarget(self)
self.preloadHistoryPeerIdDisposable.dispose()
self.preloadNextChatPeerIdDisposable.dispose()
self.reportIrrelvantGeoDisposable?.dispose()
self.reminderActivity?.invalidate()
self.updateSlowmodeStatusDisposable.dispose()
self.keepPeerInfoScreenDataHotDisposable.dispose()
self.preloadAvatarDisposable.dispose()
self.peekTimerDisposable.dispose()
self.hasActiveGroupCallDisposable?.dispose()
self.createVoiceChatDisposable.dispose()
self.checksTooltipDisposable.dispose()
self.peerSuggestionsDisposable.dispose()
self.peerSuggestionsDismissDisposable.dispose()
self.selectAddMemberDisposable.dispose()
self.addMemberDisposable.dispose()
self.joinChannelDisposable.dispose()
self.nextChannelToReadDisposable?.dispose()
self.inviteRequestsDisposable.dispose()
self.sendAsPeersDisposable?.dispose()
self.preloadAttachBotIconsDisposables?.dispose()
self.keepMessageCountersSyncrhonizedDisposable?.dispose()
self.keepSavedMessagesSyncrhonizedDisposable?.dispose()
self.translationStateDisposable?.dispose()
self.premiumGiftSuggestionDisposable?.dispose()
self.powerSavingMonitoringDisposable?.dispose()
self.saveMediaDisposable?.dispose()
self.giveawayStatusDisposable?.dispose()
self.nameColorDisposable?.dispose()
self.choosingStickerActivityDisposable?.dispose()
self.automaticMediaDownloadSettingsDisposable?.dispose()
self.stickerSettingsDisposable?.dispose()
self.searchQuerySuggestionState?.1.dispose()
self.preloadSavedMessagesChatsDisposable?.dispose()
self.recorderDataDisposable.dispose()
self.displaySendWhenOnlineTipDisposable.dispose()
self.networkSpeedEventsDisposable?.dispose()
}
deallocate()
}
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
self.updateChatPresentationInterfaceState(animated: false, interactive: false, {
return $0.updatedMode(mode)
})
}
var chatDisplayNode: ChatControllerNode {
get {
return super.displayNode as! ChatControllerNode
}
}
func themeAndStringsUpdated() {
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
switch self.presentationInterfaceState.mode {
case let .standard(standardMode):
switch standardMode {
case .embedded:
self.statusBar.statusBarStyle = .Ignore
default:
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.deferScreenEdgeGestures = []
}
case .overlay:
self.statusBar.statusBarStyle = .Hide
self.deferScreenEdgeGestures = [.top]
case .inline:
self.statusBar.statusBarStyle = .Ignore
}
self.updateNavigationBarPresentation()
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
var state = state
state = state.updatedPresentationReady(self.didSetPresentationData)
state = state.updatedTheme(self.presentationData.theme)
state = state.updatedStrings(self.presentationData.strings)
state = state.updatedDateTimeFormat(self.presentationData.dateTimeFormat)
state = state.updatedChatWallpaper(self.presentationData.chatWallpaper)
state = state.updatedBubbleCorners(self.presentationData.chatBubbleCorners)
return state
})
self.currentContextController?.updateTheme(presentationData: self.presentationData)
}
func updateNavigationBarPresentation() {
let navigationBarTheme: NavigationBarTheme
let presentationTheme: PresentationTheme
if let forcedNavigationBarTheme = self.forcedNavigationBarTheme {
presentationTheme = forcedNavigationBarTheme
navigationBarTheme = NavigationBarTheme(rootControllerTheme: forcedNavigationBarTheme, hideBackground: false, hideBadge: true)
} else if self.hasEmbeddedTitleContent {
presentationTheme = self.presentationData.theme
navigationBarTheme = NavigationBarTheme(rootControllerTheme: defaultDarkPresentationTheme, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: true)
} else {
presentationTheme = self.presentationData.theme
navigationBarTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: false)
}
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
self.chatTitleView?.updateThemeAndStrings(theme: presentationTheme, strings: self.presentationData.strings, hasEmbeddedTitleContent: self.hasEmbeddedTitleContent)
}
func topPinnedMessageSignal(latest: Bool) -> Signal<ChatPinnedMessage?, NoError> {
var pinnedPeerId: EnginePeer.Id?
let threadId = self.chatLocation.threadId
let loadState: Signal<Bool, NoError> = self.chatDisplayNode.historyNode.historyState.get()
|> map { state -> Bool in
switch state {
case .loading:
return false
default:
return true
}
}
|> distinctUntilChanged
switch self.chatLocation {
case let .peer(id):
pinnedPeerId = id
case let .replyThread(message):
if message.isForumPost {
pinnedPeerId = self.chatLocation.peerId
}
default:
break
}
if let peerId = pinnedPeerId {
let topPinnedMessage: Signal<ChatPinnedMessage?, NoError>
enum ReferenceMessage {
struct Loaded {
var id: MessageId
var minId: MessageId
var isScrolled: Bool
}
case ready(Loaded)
case loading
}
let referenceMessage: Signal<ReferenceMessage?, NoError>
if latest {
referenceMessage = .single(nil)
} else {
referenceMessage = combineLatest(
queue: Queue.mainQueue(),
self.scrolledToMessageId.get(),
self.chatDisplayNode.historyNode.topVisibleMessageRange.get()
)
|> map { scrolledToMessageId, topVisibleMessageRange -> ReferenceMessage? in
if let topVisibleMessageRange = topVisibleMessageRange, topVisibleMessageRange.isLoading {
return .loading
}
let bottomVisibleMessage = topVisibleMessageRange?.lowerBound.id
let topVisibleMessage = topVisibleMessageRange?.upperBound.id
if let scrolledToMessageId = scrolledToMessageId {
if let topVisibleMessage, let bottomVisibleMessage {
if scrolledToMessageId.allowedReplacementDirection.contains(.up) && topVisibleMessage < scrolledToMessageId.id {
return .ready(ReferenceMessage.Loaded(id: topVisibleMessage, minId: bottomVisibleMessage, isScrolled: false))
}
}
return .ready(ReferenceMessage.Loaded(id: scrolledToMessageId.id, minId: scrolledToMessageId.id, isScrolled: true))
} else if let topVisibleMessage, let bottomVisibleMessage {
return .ready(ReferenceMessage.Loaded(id: topVisibleMessage, minId: bottomVisibleMessage, isScrolled: false))
} else {
return nil
}
}
}
let context = self.context
func pinnedHistorySignal(anchorMessageId: MessageId?, count: Int) -> Signal<ChatHistoryViewUpdate, NoError> {
let location: ChatHistoryLocation
if let anchorMessageId = anchorMessageId {
location = .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(anchorMessageId), quote: nil), count: count, highlight: false)
} else {
location = .Initial(count: count)
}
let chatLocation: ChatLocation
if let threadId {
chatLocation = .replyThread(message: ChatReplyThreadMessage(peerId: peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false))
} else {
chatLocation = .peer(id: peerId)
}
return (chatHistoryViewForLocation(ChatHistoryLocationInput(content: location, id: 0), ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), context: context, chatLocation: chatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), scheduled: false, fixedCombinedReadStates: nil, tag: .tag(MessageTags.pinned), appendMessagesFromTheSameGroup: false, additionalData: [], orderStatistics: .combinedLocation)
|> castError(Bool.self)
|> mapToSignal { update -> Signal<ChatHistoryViewUpdate, Bool> in
switch update {
case let .Loading(_, type):
if case .Generic(.FillHole) = type {
return .fail(true)
}
case let .HistoryView(_, type, _, _, _, _, _):
if case .Generic(.FillHole) = type {
return .fail(true)
}
}
return .single(update)
})
|> restartIfError
}
struct TopMessage {
var message: Message
var index: Int
}
let topMessage = pinnedHistorySignal(anchorMessageId: nil, count: 10)
|> map { update -> TopMessage? in
switch update {
case .Loading:
return nil
case let .HistoryView(viewValue, _, _, _, _, _, _):
if let entry = viewValue.entries.last {
let index: Int
if let location = entry.location {
index = location.index
} else {
index = viewValue.entries.count - 1
}
return TopMessage(
message: entry.message,
index: index
)
} else {
return nil
}
}
}
let loadCount = 10
struct PinnedHistory {
struct PinnedMessage {
var message: Message
var index: Int
}
var messages: [PinnedMessage]
var totalCount: Int
}
let adjustedReplyHistory: Signal<PinnedHistory, NoError>
if latest {
adjustedReplyHistory = pinnedHistorySignal(anchorMessageId: nil, count: loadCount)
|> map { view -> PinnedHistory in
switch view {
case .Loading:
return PinnedHistory(messages: [], totalCount: 0)
case let .HistoryView(viewValue, _, _, _, _, _, _):
var messages: [PinnedHistory.PinnedMessage] = []
var totalCount = viewValue.entries.count
for i in 0 ..< viewValue.entries.count {
let index: Int
if !viewValue.holeEarlier && viewValue.earlierId == nil {
index = i
} else if let location = viewValue.entries[i].location {
index = location.index
totalCount = location.count
} else {
index = i
}
messages.append(PinnedHistory.PinnedMessage(
message: viewValue.entries[i].message,
index: index
))
}
return PinnedHistory(messages: messages, totalCount: totalCount)
}
}
} else {
adjustedReplyHistory = (Signal<PinnedHistory, NoError> { subscriber in
var referenceMessageValue: ReferenceMessage?
var view: ChatHistoryViewUpdate?
let updateState: () -> Void = {
guard let view = view else {
return
}
guard case let .HistoryView(viewValue, _, _, _, _, _, _) = view else {
subscriber.putNext(PinnedHistory(messages: [], totalCount: 0))
return
}
var messages: [PinnedHistory.PinnedMessage] = []
for i in 0 ..< viewValue.entries.count {
messages.append(PinnedHistory.PinnedMessage(
message: viewValue.entries[i].message,
index: i
))
}
let result = PinnedHistory(messages: messages, totalCount: messages.count)
if case let .ready(loaded) = referenceMessageValue {
let referenceId = loaded.id
if viewValue.entries.count < loadCount {
subscriber.putNext(result)
} else if referenceId < viewValue.entries[1].message.id {
if viewValue.earlierId != nil {
subscriber.putCompletion()
} else {
subscriber.putNext(result)
}
} else if referenceId > viewValue.entries[viewValue.entries.count - 2].message.id {
if viewValue.laterId != nil {
subscriber.putCompletion()
} else {
subscriber.putNext(result)
}
} else {
subscriber.putNext(result)
}
} else {
if viewValue.isLoading {
subscriber.putNext(result)
} else if viewValue.holeLater || viewValue.laterId != nil {
subscriber.putCompletion()
} else {
subscriber.putNext(result)
}
}
}
var initializedView = false
let viewDisposable = MetaDisposable()
let referenceDisposable = (referenceMessage
|> deliverOnMainQueue).startStrict(next: { referenceMessage in
referenceMessageValue = referenceMessage
if !initializedView {
initializedView = true
//print("reload at \(String(describing: referenceMessage?.id)) disposable \(unsafeBitCast(viewDisposable, to: UInt64.self))")
var referenceId: MessageId?
if case let .ready(loaded) = referenceMessage {
referenceId = loaded.id
}
viewDisposable.set((pinnedHistorySignal(anchorMessageId: referenceId, count: loadCount)
|> deliverOnMainQueue).startStrict(next: { next in
view = next
updateState()
}))
}
updateState()
})
return ActionDisposable {
//print("dispose \(unsafeBitCast(viewDisposable, to: UInt64.self))")
referenceDisposable.dispose()
viewDisposable.dispose()
}
}
|> runOn(.mainQueue()))
|> restart
}
topPinnedMessage = combineLatest(queue: .mainQueue(),
adjustedReplyHistory,
topMessage,
referenceMessage,
loadState
)
|> map { pinnedMessages, topMessage, referenceMessage, loadState -> ChatPinnedMessage? in
if !loadState {
return nil
}
var message: ChatPinnedMessage?
let topMessageId: MessageId
if pinnedMessages.messages.isEmpty {
return nil
}
topMessageId = topMessage?.message.id ?? pinnedMessages.messages[pinnedMessages.messages.count - 1].message.id
if case let .ready(referenceMessage) = referenceMessage, referenceMessage.isScrolled, !pinnedMessages.messages.isEmpty, referenceMessage.id == pinnedMessages.messages[0].message.id, let topMessage = topMessage {
var index = topMessage.index
for message in pinnedMessages.messages {
if message.message.id == topMessage.message.id {
index = message.index
break
}
}
if threadId != nil {
if referenceMessage.minId <= topMessage.message.id {
return nil
}
}
return ChatPinnedMessage(message: topMessage.message, index: index, totalCount: pinnedMessages.totalCount, topMessageId: topMessageId)
}
//print("reference: \(String(describing: referenceMessage?.id.id)) entries: \(view.entries.map(\.index.id.id))")
for i in 0 ..< pinnedMessages.messages.count {
let entry = pinnedMessages.messages[i]
var matches = false
if message == nil {
matches = true
} else if case let .ready(referenceMessage) = referenceMessage {
if referenceMessage.isScrolled {
if entry.message.id < referenceMessage.id {
matches = true
}
} else {
if entry.message.id <= referenceMessage.id {
matches = true
}
}
} else {
matches = true
}
if matches {
if threadId != nil, case let .ready(referenceMessage) = referenceMessage {
if referenceMessage.minId <= entry.message.id {
continue
}
}
message = ChatPinnedMessage(message: entry.message, index: entry.index, totalCount: pinnedMessages.totalCount, topMessageId: topMessageId)
}
}
return message
}
|> distinctUntilChanged
return topPinnedMessage
} else {
return .single(nil)
}
}
var storedAnimateFromSnapshotState: ChatControllerNode.SnapshotState?
func animateFromPreviousController(snapshotState: ChatControllerNode.SnapshotState) {
self.storedAnimateFromSnapshotState = snapshotState
}
override public func loadDisplayNode() {
self.loadDisplayNodeImpl()
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if self.willAppear {
self.chatDisplayNode.historyNode.refreshPollActionsForVisibleMessages()
} else {
self.willAppear = true
// Limit this to reply threads just to be safe now
if case .replyThread = self.chatLocation {
self.chatDisplayNode.historyNode.refocusOnUnreadMessagesIfNeeded()
}
}
if case let .replyThread(message) = self.chatLocation, message.isForumPost {
if self.keepMessageCountersSyncrhonizedDisposable == nil {
self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: message.peerId, threadId: message.threadId).startStrict()
}
} else if self.chatLocation.peerId == self.context.account.peerId {
if self.keepMessageCountersSyncrhonizedDisposable == nil {
if let threadId = self.chatLocation.threadId {
self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: self.context.account.peerId, threadId: threadId).startStrict()
} else {
self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: self.context.account.peerId).startStrict()
}
}
if self.keepSavedMessagesSyncrhonizedDisposable == nil {
self.keepSavedMessagesSyncrhonizedDisposable = self.context.engine.stickers.refreshSavedMessageTags(subPeerId: self.chatLocation.threadId.flatMap(PeerId.init)).startStrict()
}
}
if let scheduledActivateInput = scheduledActivateInput, case .text = scheduledActivateInput {
self.scheduledActivateInput = nil
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
return state.updatedInputMode({ _ in
switch scheduledActivateInput {
case .text:
return .text
case .entityInput:
return .media(mode: .other, expanded: nil, focused: false)
}
})
})
}
var chatNavigationStack: [ChatNavigationStackItem] = self.chatNavigationStack
if let peerId = self.chatLocation.peerId {
if let summary = self.customNavigationDataSummary as? ChatControllerNavigationDataSummary {
chatNavigationStack.removeAll()
chatNavigationStack = summary.peerNavigationItems.filter({ $0 != ChatNavigationStackItem(peerId: peerId, threadId: self.chatLocation.threadId) })
}
if let _ = self.chatLocation.threadId {
if !chatNavigationStack.contains(ChatNavigationStackItem(peerId: peerId, threadId: nil)) {
chatNavigationStack.append(ChatNavigationStackItem(peerId: peerId, threadId: nil))
}
}
}
if !chatNavigationStack.isEmpty {
self.chatDisplayNode.navigationBar?.backButtonNode.isGestureEnabled = true
self.chatDisplayNode.navigationBar?.backButtonNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let backButtonNode = strongSelf.chatDisplayNode.navigationBar?.backButtonNode, let navigationController = strongSelf.effectiveNavigationController else {
gesture.cancel()
return
}
let nextFolderId: Int32? = strongSelf.currentChatListFilter
PeerInfoScreenImpl.displayChatNavigationMenu(
context: strongSelf.context,
chatNavigationStack: chatNavigationStack,
nextFolderId: nextFolderId,
parentController: strongSelf,
backButtonView: backButtonNode.view,
navigationController: navigationController,
gesture: gesture
)
}
}
}
var returnInputViewFocus = false
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.didAppear = true
self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false
self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in
return a && b
})
self.chatDisplayNode.loadInputPanels(theme: self.presentationInterfaceState.theme, strings: self.presentationInterfaceState.strings, fontSize: self.presentationInterfaceState.fontSize)
if self.recentlyUsedInlineBotsDisposable == nil {
self.recentlyUsedInlineBotsDisposable = (self.context.engine.peers.recentlyUsedInlineBots() |> deliverOnMainQueue).startStrict(next: { [weak self] peers in
self?.recentlyUsedInlineBotsValue = peers.filter({ $0.1 >= 0.14 }).map({ $0.0._asPeer() })
})
}
if case .standard(.default) = self.presentationInterfaceState.mode, self.raiseToListen == nil {
self.raiseToListen = RaiseToListenManager(shouldActivate: { [weak self] in
if let strongSelf = self, strongSelf.isNodeLoaded && strongSelf.canReadHistoryValue, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil, strongSelf.playlistStateAndType == nil {
if !strongSelf.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak }) {
return false
}
if strongSelf.effectiveNavigationController?.topViewController !== strongSelf {
return false
}
if strongSelf.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil {
return false
}
if !strongSelf.traceVisibility() {
return false
}
if strongSelf.currentContextController != nil {
return false
}
if !isTopmostChatController(strongSelf) {
return false
}
if strongSelf.firstLoadedMessageToListen() != nil || strongSelf.chatDisplayNode.isTextInputPanelActive {
if strongSelf.context.sharedContext.immediateHasOngoingCall {
return false
}
if case .media = strongSelf.presentationInterfaceState.inputMode {
return false
}
return true
}
}
return false
}, activate: { [weak self] in
self?.activateRaiseGesture()
}, deactivate: { [weak self] in
self?.deactivateRaiseGesture()
})
self.raiseToListen?.enabled = self.canReadHistoryValue
self.tempVoicePlaylistEnded = { [weak self] in
guard let strongSelf = self else {
return
}
if !canSendMessagesToChat(strongSelf.presentationInterfaceState) {
return
}
if let raiseToListen = strongSelf.raiseToListen {
strongSelf.voicePlaylistDidEndTimestamp = CACurrentMediaTime()
raiseToListen.activateBasedOnProximity(delay: 0.0)
}
if strongSelf.returnInputViewFocus {
strongSelf.returnInputViewFocus = false
strongSelf.chatDisplayNode.ensureInputViewFocused()
}
}
self.tempVoicePlaylistItemChanged = { [weak self] previousItem, currentItem in
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.historyNode.voicePlaylistItemChanged(previousItem, currentItem)
}
}
if let arguments = self.presentationArguments as? ChatControllerOverlayPresentationData {
//TODO clear arguments
self.chatDisplayNode.animateInAsOverlay(from: arguments.expandData.0, completion: {
arguments.expandData.1()
})
}
if !self.didSetup3dTouch {
self.didSetup3dTouch = true
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
let dropInteraction = UIDropInteraction(delegate: self)
self.chatDisplayNode.view.addInteraction(dropInteraction)
}
}
if !self.checkedPeerChatServiceActions {
self.checkedPeerChatServiceActions = true
if case let .peer(peerId) = self.chatLocation, self.screenCaptureManager == nil {
if peerId.namespace == Namespaces.Peer.SecretChat {
self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in
if let strongSelf = self, strongSelf.traceVisibility() {
if strongSelf.canReadHistoryValue {
let _ = strongSelf.context.engine.messages.addSecretChatMessageScreenshot(peerId: peerId).startStandalone()
}
return true
} else {
return false
}
})
} else if peerId.namespace == Namespaces.Peer.CloudUser && peerId.id._internalGetInt64Value() == 777000 {
self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in
if let strongSelf = self, strongSelf.traceVisibility() {
let loginCodeRegex = try? NSRegularExpression(pattern: "[\\d\\-]{5,7}", options: [])
var loginCodesToInvalidate: [String] = []
strongSelf.chatDisplayNode.historyNode.forEachVisibleMessageItemNode({ itemNode in
if let text = itemNode.item?.message.text, let matches = loginCodeRegex?.matches(in: text, options: [], range: NSMakeRange(0, (text as NSString).length)), let match = matches.first {
loginCodesToInvalidate.append((text as NSString).substring(with: match.range))
}
})
if !loginCodesToInvalidate.isEmpty {
let _ = strongSelf.context.engine.auth.invalidateLoginCodes(codes: loginCodesToInvalidate).startStandalone()
}
return true
} else {
return false
}
})
} else if peerId.namespace == Namespaces.Peer.CloudUser {
self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in
guard let self else {
return false
}
let _ = (self.context.sharedContext.mediaManager.globalMediaPlayerState
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] playlistStateAndType in
if let self, let (_, playbackState, _) = playlistStateAndType, case let .state(state) = playbackState {
if let source = state.item.playbackData?.source, case let .telegramFile(_, _, isViewOnce) = source, isViewOnce {
self.context.sharedContext.mediaManager.setPlaylist(nil, type: .voice, control: .playback(.pause))
}
}
})
return true
})
}
}
if case let .peer(peerId) = self.chatLocation {
let _ = self.context.engine.peers.checkPeerChatServiceActions(peerId: peerId).startStandalone()
}
if self.chatLocation.peerId != nil && self.chatDisplayNode.frameForInputActionButton() != nil {
let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string
if !inputText.isEmpty {
if inputText.count > 4 {
let _ = (ApplicationSpecificNotice.getChatMessageOptionsTip(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).startStandalone(next: { [weak self] counter in
if let strongSelf = self, counter < 3 {
let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone()
strongSelf.displaySendingOptionsTooltip()
}
})
}
} else if self.presentationInterfaceState.interfaceState.mediaRecordingMode == .audio {
var canSendMedia = false
if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendMedia) == nil && channel.hasBannedPermission(.banSendVoice) == nil {
canSendMedia = true
}
} else if let group = self.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup {
if !group.hasBannedPermission(.banSendMedia) && !group.hasBannedPermission(.banSendVoice) {
canSendMedia = true
}
} else {
canSendMedia = true
}
if canSendMedia && self.presentationInterfaceState.voiceMessagesAvailable {
let _ = (ApplicationSpecificNotice.getChatMediaMediaRecordingTips(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).startStandalone(next: { [weak self] counter in
guard let strongSelf = self else {
return
}
var displayTip = false
if counter == 0 {
displayTip = true
} else if counter < 3 && arc4random_uniform(4) == 1 {
displayTip = true
}
if displayTip {
let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone()
strongSelf.displayMediaRecordingTooltip()
}
})
}
}
}
self.editMessageErrorsDisposable.set((self.context.account.pendingUpdateMessageManager.errors
|> deliverOnMainQueue).startStrict(next: { [weak self] (_, error) in
guard let strongSelf = self else {
return
}
let text: String
switch error {
case .generic, .textTooLong, .invalidGrouping:
text = strongSelf.presentationData.strings.Channel_EditMessageErrorGeneric
case .restricted:
text = strongSelf.presentationData.strings.Group_ErrorSendRestrictedMedia
}
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
})]), in: .window(.root))
}))
if case let .peer(peerId) = self.chatLocation {
let context = self.context
self.keepPeerInfoScreenDataHotDisposable.set(keepPeerInfoScreenDataHot(context: context, peerId: peerId, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder).startStrict())
if peerId.namespace == Namespaces.Peer.CloudUser {
self.preloadAvatarDisposable.set((peerInfoProfilePhotosWithCache(context: context, peerId: peerId)
|> mapToSignal { (complete, result) -> Signal<Never, NoError> in
var signals: [Signal<Never, NoError>] = [.complete()]
for i in 0 ..< min(1, result.count) {
if let video = result[i].videoRepresentations.first {
let duration: Double = (video.representation.startTimestamp ?? 0.0) + (i == 0 ? 4.0 : 2.0)
signals.append(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: video.reference, duration: duration))
}
}
return combineLatest(signals) |> mapToSignal { _ in
return .never()
}
}).startStrict())
}
}
self.preloadAttachBotIconsDisposables = AttachmentController.preloadAttachBotIcons(context: self.context)
}
if let _ = self.focusOnSearchAfterAppearance {
self.focusOnSearchAfterAppearance = nil
if let searchNode = self.navigationBar?.contentNode as? ChatSearchNavigationContentNode {
searchNode.activate()
}
}
if let peekData = self.peekData, case let .peer(peerId) = self.chatLocation {
let timestamp = Int32(Date().timeIntervalSince1970)
let remainingTime = max(1, peekData.deadline - timestamp)
self.peekTimerDisposable.set((
combineLatest(
self.context.account.postbox.peerView(id: peerId),
Signal<Bool, NoError>.single(true)
|> suspendAwareDelay(Double(remainingTime), granularity: 2.0, queue: .mainQueue())
)
|> deliverOnMainQueue
).startStrict(next: { [weak self] peerView, _ in
guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else {
return
}
if let peer = peer as? TelegramChannel {
switch peer.participationStatus {
case .member:
return
default:
break
}
}
strongSelf.present(textAlertController(
context: strongSelf.context,
title: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertTitle,
text: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertText,
actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertJoin, action: {
guard let strongSelf = self else {
return
}
strongSelf.peekTimerDisposable.set(
(strongSelf.context.engine.peers.joinChatInteractively(with: peekData.linkData)
|> deliverOnMainQueue).startStrict(next: { peerId in
guard let strongSelf = self else {
return
}
if peerId == nil {
strongSelf.dismiss()
}
}, error: { _ in
guard let strongSelf = self else {
return
}
strongSelf.dismiss()
})
)
}),
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
guard let strongSelf = self else {
return
}
strongSelf.dismiss()
})
],
actionLayout: .vertical,
dismissOnOutsideTap: false
), in: .window(.root))
}))
}
self.checksTooltipDisposable.set((self.context.engine.notices.getServerProvidedSuggestions()
|> deliverOnMainQueue).startStrict(next: { [weak self] values in
guard let strongSelf = self, strongSelf.chatLocation.peerId != strongSelf.context.account.peerId else {
return
}
if !values.contains(.newcomerTicks) {
return
}
strongSelf.shouldDisplayChecksTooltip = true
}))
if case let .peer(peerId) = self.chatLocation {
self.peerSuggestionsDisposable.set((self.context.engine.notices.getPeerSpecificServerProvidedSuggestions(peerId: peerId)
|> deliverOnMainQueue).startStrict(next: { [weak self] values in
guard let strongSelf = self else {
return
}
if !strongSelf.traceVisibility() || strongSelf.navigationController?.topViewController != strongSelf {
return
}
if values.contains(.convertToGigagroup) && !strongSelf.displayedConvertToGigagroupSuggestion {
strongSelf.displayedConvertToGigagroupSuggestion = true
let attributedTitle = NSAttributedString(string: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_Title, font: Font.semibold(strongSelf.presentationData.listsFontSize.baseDisplaySize), textColor: strongSelf.presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
let body = MarkdownAttributeSet(font: Font.regular(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: strongSelf.presentationData.theme.actionSheet.primaryTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: strongSelf.presentationData.theme.actionSheet.primaryTextColor)
let participantsLimit = strongSelf.context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount
let text = strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_Text(presentationStringsFormattedNumber(participantsLimit, strongSelf.presentationData.dateTimeFormat.groupingSeparator)).string
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
let controller = richTextAlertController(context: strongSelf.context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_SettingsTip, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_LearnMore, action: {
let context = strongSelf.context
let presentationData = strongSelf.presentationData
let controller = PermissionController(context: context, splashScreen: true)
controller.navigationPresentation = .modal
controller.setState(.custom(icon: .animation("BroadcastGroup"), title: presentationData.strings.BroadcastGroups_IntroTitle, subtitle: nil, text: presentationData.strings.BroadcastGroups_IntroText, buttonTitle: presentationData.strings.BroadcastGroups_Convert, secondaryButtonTitle: presentationData.strings.BroadcastGroups_Cancel, footerText: nil), animated: false)
controller.proceed = { [weak controller] result in
let attributedTitle = NSAttributedString(string: presentationData.strings.BroadcastGroups_ConfirmationAlert_Title, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
let body = MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor)
let attributedText = parseMarkdownIntoAttributedString(presentationData.strings.BroadcastGroups_ConfirmationAlert_Text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
let alertController = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
let _ = context.engine.notices.dismissPeerSpecificServerProvidedSuggestion(peerId: peerId, suggestion: .convertToGigagroup).startStandalone()
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Convert, action: { [weak controller] in
controller?.dismiss()
let _ = context.engine.notices.dismissPeerSpecificServerProvidedSuggestion(peerId: peerId, suggestion: .convertToGigagroup).startStandalone()
let _ = (convertGroupToGigagroup(account: context.account, peerId: peerId)
|> deliverOnMainQueue).startStandalone(completed: {
let participantsLimit = context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .gigagroupConversion(text: presentationData.strings.BroadcastGroups_Success(presentationStringsFormattedNumber(participantsLimit, presentationData.dateTimeFormat.decimalSeparator)).string), elevatedLayout: false, action: { _ in return false }), in: .current)
})
})])
controller?.present(alertController, in: .window(.root))
}
strongSelf.push(controller)
})])
strongSelf.present(controller, in: .window(.root))
}
}))
}
if let scheduledActivateInput = self.scheduledActivateInput {
self.scheduledActivateInput = nil
switch scheduledActivateInput {
case .text:
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
return state.updatedInputMode({ _ in
return .text
})
})
case .entityInput:
self.chatDisplayNode.openStickers(beginWithEmoji: true)
}
}
if let snapshotState = self.storedAnimateFromSnapshotState {
self.storedAnimateFromSnapshotState = nil
if let titleViewSnapshotState = snapshotState.titleViewSnapshotState {
self.chatTitleView?.animateFromSnapshot(titleViewSnapshotState)
}
if let avatarSnapshotState = snapshotState.avatarSnapshotState {
(self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshotState)
}
self.chatDisplayNode.animateFromSnapshot(snapshotState, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.historyNode.preloadPages = true
})
} else {
self.chatDisplayNode.historyNode.preloadPages = true
}
if let attachBotStart = self.attachBotStart {
self.attachBotStart = nil
self.presentAttachmentBot(botId: attachBotStart.botId, payload: attachBotStart.payload, justInstalled: attachBotStart.justInstalled)
}
if self.powerSavingMonitoringDisposable == nil {
self.powerSavingMonitoringDisposable = (self.context.sharedContext.automaticMediaDownloadSettings
|> mapToSignal { settings -> Signal<Bool, NoError> in
return automaticEnergyUsageShouldBeOn(settings: settings)
}
|> distinctUntilChanged).startStrict(next: { [weak self] isPowerSavingEnabled in
guard let self else {
return
}
var previousValueValue: Bool?
previousValueValue = ChatListControllerImpl.sharedPreviousPowerSavingEnabled
ChatListControllerImpl.sharedPreviousPowerSavingEnabled = isPowerSavingEnabled
/*#if DEBUG
previousValueValue = false
#endif*/
if isPowerSavingEnabled != previousValueValue && previousValueValue != nil && isPowerSavingEnabled {
let batteryLevel = UIDevice.current.batteryLevel
if batteryLevel > 0.0 && self.view.window != nil {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let batteryPercentage = Int(batteryLevel * 100.0)
self.dismissAllUndoControllers()
self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "lowbattery_30", scale: 1.0, colors: [:], title: presentationData.strings.PowerSaving_AlertEnabledTitle, text: presentationData.strings.PowerSaving_AlertEnabledText("\(batteryPercentage)").string, customUndoText: presentationData.strings.PowerSaving_AlertEnabledAction, timeout: 5.0), elevatedLayout: false, action: { [weak self] action in
if case .undo = action, let self {
let _ = updateMediaDownloadSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { settings in
var settings = settings
settings.energyUsageSettings.activationThreshold = 4
return settings
}).startStandalone()
}
return false
}), in: .current)
}
}
})
}
}
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
UIView.performWithoutAnimation {
self.view.endEditing(true)
}
self.chatDisplayNode.historyNode.canReadHistory.set(.single(false))
self.saveInterfaceState()
self.dismissAllTooltips()
self.sendMessageActionsController?.dismiss()
self.themeScreen?.dismiss()
self.attachmentController?.dismiss()
self.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
if let _ = self.peekData {
self.peekTimerDisposable.set(nil)
}
}
func saveInterfaceState(includeScrollState: Bool = true) {
if case .messageOptions = self.subject {
return
}
var includeScrollState = includeScrollState
var peerId: PeerId
var threadId: Int64?
switch self.chatLocation {
case let .peer(peerIdValue):
peerId = peerIdValue
case let .replyThread(replyThreadMessage):
if replyThreadMessage.peerId == self.context.account.peerId && replyThreadMessage.threadId == self.context.account.peerId.toInt64() {
peerId = replyThreadMessage.peerId
threadId = nil
includeScrollState = true
let scrollState = self.chatDisplayNode.historyNode.immediateScrollState()
let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: replyThreadMessage.threadId, { current in
return current.withUpdatedHistoryScrollState(scrollState)
}).startStandalone()
} else {
peerId = replyThreadMessage.peerId
threadId = replyThreadMessage.threadId
}
case .customChatContents:
return
}
let timestamp = Int32(Date().timeIntervalSince1970)
var interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp)
if includeScrollState {
let scrollState = self.chatDisplayNode.historyNode.immediateScrollState()
interfaceState = interfaceState.withUpdatedHistoryScrollState(scrollState)
}
interfaceState = interfaceState.withUpdatedInputLanguage(self.chatDisplayNode.currentTextInputLanguage)
if case .peer = self.chatLocation, let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) {
interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState()).withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil)
}
let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: threadId, { _ in
return interfaceState
}).startStandalone()
}
override public func viewWillLeaveNavigation() {
self.chatDisplayNode.willNavigateAway()
}
override public func inFocusUpdated(isInFocus: Bool) {
self.disableStickerAnimationsPromise.set(!isInFocus)
self.chatDisplayNode.inFocusUpdated(isInFocus: isInFocus)
}
func canManagePin() -> Bool {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return false
}
var canManagePin = false
if let channel = peer as? TelegramChannel {
canManagePin = channel.hasPermission(.pinMessages)
} else if let group = peer as? TelegramGroup {
switch group.role {
case .creator, .admin:
canManagePin = true
default:
if let defaultBannedRights = group.defaultBannedRights {
canManagePin = !defaultBannedRights.flags.contains(.banPinMessages)
} else {
canManagePin = true
}
}
} else if let _ = peer as? TelegramUser, self.presentationInterfaceState.explicitelyCanPinMessages {
canManagePin = true
}
return canManagePin
}
var suspendNavigationBarLayout: Bool = false
var suspendedNavigationBarLayout: ContainerViewLayout?
var additionalNavigationBarBackgroundHeight: CGFloat = 0.0
var additionalNavigationBarHitTestSlop: CGFloat = 0.0
override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
if self.suspendNavigationBarLayout {
self.suspendedNavigationBarLayout = layout
return
}
self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition)
}
override public func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
return nil
}
public func updateIsScrollingLockedAtTop(isScrollingLockedAtTop: Bool) {
self.chatDisplayNode.isScrollingLockedAtTop = isScrollingLockedAtTop
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.suspendNavigationBarLayout = true
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
self.chatTitleView?.layout = layout
switch self.presentationInterfaceState.mode {
case .standard, .inline:
break
case .overlay:
if case .Ignore = self.statusBar.statusBarStyle {
} else if layout.safeInsets.top.isZero {
self.statusBar.statusBarStyle = .Hide
} else {
self.statusBar.statusBarStyle = .Ignore
}
}
var layout = layout
if case .compact = layout.metrics.widthClass, let attachmentController = self.attachmentController, attachmentController.window != nil {
layout = layout.withUpdatedInputHeight(nil)
}
var navigationBarTransition = transition
self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in
self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop, completion: completion)
}, updateExtraNavigationBarBackgroundHeight: { value, hitTestSlop, extraNavigationTransition in
navigationBarTransition = extraNavigationTransition
self.additionalNavigationBarBackgroundHeight = value
self.additionalNavigationBarHitTestSlop = hitTestSlop
})
if case .compact = layout.metrics.widthClass {
let hasOverlayNodes = self.context.sharedContext.mediaManager.overlayMediaManager.controller?.hasNodes ?? false
if self.validLayout != nil && layout.size.width > layout.size.height && !hasOverlayNodes && self.traceVisibility() && isTopmostChatController(self) {
var completed = false
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if !completed, let itemNode = itemNode as? ChatMessageItemView, let message = itemNode.item?.message, let (_, soundEnabled, _, _, _) = itemNode.playMediaWithSound(), soundEnabled {
let _ = self.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .landscape))
completed = true
}
}
}
}
self.suspendNavigationBarLayout = false
if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout {
self.suspendedNavigationBarLayout = suspendedNavigationBarLayout
self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: navigationBarTransition)
}
self.navigationBar?.additionalContentNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: 0.0, bottom: self.additionalNavigationBarHitTestSlop, right: 0.0)
}
func updateChatPresentationInterfaceState(animated: Bool = true, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) {
self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interactive: interactive, saveInterfaceState: saveInterfaceState, f, completion: completion)
}
func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) {
updateChatPresentationInterfaceStateImpl(
selfController: self,
transition: transition,
interactive: interactive,
saveInterfaceState: saveInterfaceState,
f,
completion: completion
)
}
func updateItemNodesSelectionStates(animated: Bool) {
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateSelectionState(animated: animated)
}
}
self.chatDisplayNode.historyNode.forEachItemHeaderNode{ itemHeaderNode in
if let avatarNode = itemHeaderNode as? ChatMessageAvatarHeaderNode {
avatarNode.updateSelectionState(animated: animated)
}
}
}
func updatePollTooltipMessageState(animated: Bool) {
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageBubbleItemNode {
for contentNode in itemNode.contentNodes {
if let contentNode = contentNode as? ChatMessagePollBubbleContentNode {
contentNode.updatePollTooltipMessageState(animated: animated)
}
}
itemNode.updatePsaTooltipMessageState(animated: animated)
}
}
}
func updateItemNodesSearchTextHighlightStates() {
var searchString: String?
var resultsMessageIndices: [MessageIndex]?
if let search = self.presentationInterfaceState.search, let resultsState = search.resultsState, !resultsState.messageIndices.isEmpty {
searchString = search.query
resultsMessageIndices = resultsState.messageIndices
}
if searchString != self.controllerInteraction?.searchTextHighightState?.0 || resultsMessageIndices?.count != self.controllerInteraction?.searchTextHighightState?.1.count {
var searchTextHighightState: (String, [MessageIndex])?
if let searchString = searchString, let resultsMessageIndices = resultsMessageIndices {
searchTextHighightState = (searchString, resultsMessageIndices)
}
self.controllerInteraction?.searchTextHighightState = searchTextHighightState
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateSearchTextHighlightState()
}
}
}
}
func updateItemNodesHighlightedStates(animated: Bool) {
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateHighlightedState(animated: animated)
}
}
}
@objc func leftNavigationButtonAction() {
if let button = self.leftNavigationButton {
self.navigationButtonAction(button.action)
}
}
@objc func rightNavigationButtonAction() {
if let button = self.rightNavigationButton {
if case let .peer(peerId) = self.chatLocation, case .openChatInfo(expandAvatar: true, _) = button.action, let storyStats = self.storyStats, storyStats.unseenCount != 0, let avatarNode = self.avatarNode {
self.openStories(peerId: peerId, avatarHeaderNode: nil, avatarNode: avatarNode.avatarNode)
} else {
self.navigationButtonAction(button.action)
}
}
}
@objc func secondaryRightNavigationButtonAction() {
if let button = self.secondaryRightNavigationButton {
self.navigationButtonAction(button.action)
}
}
@objc func moreButtonPressed() {
self.moreBarButton.play()
self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil)
}
public func beginClearHistory(type: InteractiveHistoryClearingType) {
guard case let .peer(peerId) = self.chatLocation else {
return
}
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
self.chatDisplayNode.historyNode.historyAppearsCleared = true
let statusText: String
if case .scheduledMessages = self.presentationInterfaceState.subject {
statusText = self.presentationData.strings.Undo_ScheduledMessagesCleared
} else if case .forEveryone = type {
if peerId.namespace == Namespaces.Peer.CloudUser {
statusText = self.presentationData.strings.Undo_ChatClearedForBothSides
} else {
statusText = self.presentationData.strings.Undo_ChatClearedForEveryone
}
} else {
statusText = self.presentationData.strings.Undo_ChatCleared
}
self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, action: { [weak self] value in
guard let strongSelf = self else {
return false
}
if value == .commit {
let _ = strongSelf.context.engine.messages.clearHistoryInteractively(peerId: peerId, threadId: nil, type: type).startStandalone(completed: {
self?.chatDisplayNode.historyNode.historyAppearsCleared = false
})
return true
} else if value == .undo {
strongSelf.chatDisplayNode.historyNode.historyAppearsCleared = false
return true
}
return false
}), in: .current)
}
public func cancelSelectingMessages() {
self.navigationButtonAction(.cancelMessageSelection)
}
func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) {
if let message = messages.first, case let .message(text, attributes, _, maybeMediaReference, _, _, _, _, _, _) = message, let mediaReference = maybeMediaReference {
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
var entities: [MessageTextEntity] = []
for attribute in attributes {
if let entitiesAttrbute = attribute as? TextEntitiesMessageAttribute {
entities = entitiesAttrbute.entities
}
}
let attributedText = chatInputStateStringWithAppliedEntities(text, entities: entities)
var state = state
if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty {
state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference))
}
if !text.isEmpty {
state = state.updatedInterfaceState { state in
if let editMessage = state.editMessage {
return state.withUpdatedEditMessage(editMessage.withUpdatedInputState(ChatTextInputState(inputText: attributedText)))
}
return state
}
}
return state
})
self.interfaceInteraction?.editMessage()
}
}
func editMessageMediaWithLegacySignals(_ signals: [Any]) {
let _ = (legacyAssetPickerEnqueueMessages(context: self.context, account: self.context.account, signals: signals)
|> deliverOnMainQueue).startStandalone(next: { [weak self] messages in
self?.editMessageMediaWithMessages(messages.map { $0.message })
})
}
public func presentAttachmentBot(botId: PeerId, payload: String?, justInstalled: Bool) {
self.attachmentController?.dismiss(animated: true, completion: nil)
self.presentAttachmentMenu(subject: .bot(id: botId, payload: payload, justInstalled: justInstalled))
}
func displayPollSolution(solution: TelegramMediaPollResults.Solution, sourceNode: ASDisplayNode, isAutomatic: Bool) {
var maybeFoundItemNode: ChatMessageItemView?
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if sourceNode.view.isDescendant(of: itemNode.view) {
maybeFoundItemNode = itemNode
}
}
}
guard let foundItemNode = maybeFoundItemNode, let item = foundItemNode.item else {
return
}
var found = false
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
if controller.text == .entities(text: solution.text, entities: solution.entities) {
found = true
controller.dismiss()
return false
}
}
return true
})
if found {
return
}
let tooltipScreen = TooltipScreen(context: self.context, account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: solution.text, entities: solution.entities), icon: .animation(name: "anim_infotip", delay: 0.2, tintColor: nil), location: .top, shouldDismissOnTouch: { point, _ in
return .ignore
}, openActiveTextItem: { [weak self] item, action in
guard let strongSelf = self else {
return
}
switch item {
case let .url(url, concealed):
switch action {
case .tap:
strongSelf.openUrl(url, concealed: concealed)
case .longTap:
strongSelf.controllerInteraction?.longTap(.url(url), nil)
}
case let .mention(peerId, mention):
switch action {
case .tap:
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
if let strongSelf = self, let peer = peer {
strongSelf.controllerInteraction?.openPeer(peer, .default, nil, .default)
}
})
case .longTap:
strongSelf.controllerInteraction?.longTap(.peerMention(peerId, mention), nil)
}
case let .textMention(mention):
switch action {
case .tap:
strongSelf.controllerInteraction?.openPeerMention(mention, nil)
case .longTap:
strongSelf.controllerInteraction?.longTap(.mention(mention), nil)
}
case let .botCommand(command):
switch action {
case .tap:
strongSelf.controllerInteraction?.sendBotCommand(nil, command)
case .longTap:
strongSelf.controllerInteraction?.longTap(.command(command), nil)
}
case let .hashtag(hashtag):
switch action {
case .tap:
strongSelf.controllerInteraction?.openHashtag(nil, hashtag)
case .longTap:
strongSelf.controllerInteraction?.longTap(.hashtag(hashtag), nil)
}
}
})
let messageId = item.message.id
self.controllerInteraction?.currentPollMessageWithTooltip = messageId
self.updatePollTooltipMessageState(animated: !isAutomatic)
tooltipScreen.willBecomeDismissed = { [weak self] tooltipScreen in
guard let strongSelf = self else {
return
}
if strongSelf.controllerInteraction?.currentPollMessageWithTooltip == messageId {
strongSelf.controllerInteraction?.currentPollMessageWithTooltip = nil
strongSelf.updatePollTooltipMessageState(animated: true)
}
}
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
self.present(tooltipScreen, in: .current)
}
public func displayPromoAnnouncement(text: String) {
let psaText: String = text
let psaEntities: [MessageTextEntity] = generateTextEntities(psaText, enabledTypes: .allUrl)
var found = false
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
if controller.text == .plain(text: psaText) {
found = true
controller.dismiss()
return false
}
}
return true
})
if found {
return
}
let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: psaText, entities: psaEntities), icon: .animation(name: "anim_infotip", delay: 0.2, tintColor: nil), location: .top, displayDuration: .custom(10.0), shouldDismissOnTouch: { point, _ in
return .ignore
}, openActiveTextItem: { [weak self] item, action in
guard let strongSelf = self else {
return
}
switch item {
case let .url(url, concealed):
switch action {
case .tap:
strongSelf.openUrl(url, concealed: concealed)
case .longTap:
strongSelf.controllerInteraction?.longTap(.url(url), nil)
}
case let .mention(peerId, mention):
switch action {
case .tap:
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
if let strongSelf = self, let peer = peer {
strongSelf.controllerInteraction?.openPeer(peer, .default, nil, .default)
}
})
case .longTap:
strongSelf.controllerInteraction?.longTap(.peerMention(peerId, mention), nil)
}
case let .textMention(mention):
switch action {
case .tap:
strongSelf.controllerInteraction?.openPeerMention(mention, nil)
case .longTap:
strongSelf.controllerInteraction?.longTap(.mention(mention), nil)
}
case let .botCommand(command):
switch action {
case .tap:
strongSelf.controllerInteraction?.sendBotCommand(nil, command)
case .longTap:
strongSelf.controllerInteraction?.longTap(.command(command), nil)
}
case let .hashtag(hashtag):
switch action {
case .tap:
strongSelf.controllerInteraction?.openHashtag(nil, hashtag)
case .longTap:
strongSelf.controllerInteraction?.longTap(.hashtag(hashtag), nil)
}
}
})
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
self.present(tooltipScreen, in: .current)
}
func displayPsa(type: String, sourceNode: ASDisplayNode, isAutomatic: Bool) {
var maybeFoundItemNode: ChatMessageItemView?
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if sourceNode.view.isDescendant(of: itemNode.view) {
maybeFoundItemNode = itemNode
}
}
}
guard let foundItemNode = maybeFoundItemNode, let item = foundItemNode.item else {
return
}
var psaText = self.presentationData.strings.Chat_GenericPsaTooltip
let key = "Chat.PsaTooltip.\(type)"
if let string = self.presentationData.strings.primaryComponent.dict[key] {
psaText = string
} else if let string = self.presentationData.strings.secondaryComponent?.dict[key] {
psaText = string
}
let psaEntities: [MessageTextEntity] = generateTextEntities(psaText, enabledTypes: .allUrl)
let messageId = item.message.id
var found = false
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
if controller.text == .plain(text: psaText) {
found = true
controller.resetDismissTimeout()
controller.willBecomeDismissed = { [weak self] tooltipScreen in
guard let strongSelf = self else {
return
}
if strongSelf.controllerInteraction?.currentPsaMessageWithTooltip == messageId {
strongSelf.controllerInteraction?.currentPsaMessageWithTooltip = nil
strongSelf.updatePollTooltipMessageState(animated: true)
}
}
return false
}
}
return true
})
if found {
self.controllerInteraction?.currentPsaMessageWithTooltip = messageId
self.updatePollTooltipMessageState(animated: !isAutomatic)
return
}
let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: psaText, entities: psaEntities), icon: .animation(name: "anim_infotip", delay: 0.2, tintColor: nil), location: .top, displayDuration: .custom(10.0), shouldDismissOnTouch: { point, _ in
return .ignore
}, openActiveTextItem: { [weak self] item, action in
guard let strongSelf = self else {
return
}
switch item {
case let .url(url, concealed):
switch action {
case .tap:
strongSelf.openUrl(url, concealed: concealed)
case .longTap:
strongSelf.controllerInteraction?.longTap(.url(url), nil)
}
case let .mention(peerId, mention):
switch action {
case .tap:
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
if let strongSelf = self, let peer = peer {
strongSelf.controllerInteraction?.openPeer(peer, .default, nil, .default)
}
})
case .longTap:
strongSelf.controllerInteraction?.longTap(.peerMention(peerId, mention), nil)
}
case let .textMention(mention):
switch action {
case .tap:
strongSelf.controllerInteraction?.openPeerMention(mention, nil)
case .longTap:
strongSelf.controllerInteraction?.longTap(.mention(mention), nil)
}
case let .botCommand(command):
switch action {
case .tap:
strongSelf.controllerInteraction?.sendBotCommand(nil, command)
case .longTap:
strongSelf.controllerInteraction?.longTap(.command(command), nil)
}
case let .hashtag(hashtag):
switch action {
case .tap:
strongSelf.controllerInteraction?.openHashtag(nil, hashtag)
case .longTap:
strongSelf.controllerInteraction?.longTap(.hashtag(hashtag), nil)
}
}
})
self.controllerInteraction?.currentPsaMessageWithTooltip = messageId
self.updatePollTooltipMessageState(animated: !isAutomatic)
tooltipScreen.willBecomeDismissed = { [weak self] tooltipScreen in
guard let strongSelf = self else {
return
}
if strongSelf.controllerInteraction?.currentPsaMessageWithTooltip == messageId {
strongSelf.controllerInteraction?.currentPsaMessageWithTooltip = nil
strongSelf.updatePollTooltipMessageState(animated: true)
}
}
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
self.present(tooltipScreen, in: .current)
}
func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return nil
}
return createPollController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), isQuiz: isQuiz, completion: { [weak self] poll in
guard let strongSelf = self else {
return
}
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }
})
}
}, nil)
let message: EnqueueMessage = .message(
text: "",
attributes: [],
inlineStickers: [:],
mediaReference: .standalone(media: TelegramMediaPoll(
pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min ... Int64.max)),
publicity: poll.publicity,
kind: poll.kind,
text: poll.text.string,
textEntities: poll.text.entities,
options: poll.options,
correctAnswers: poll.correctAnswers,
results: poll.results,
isClosed: false,
deadlineTimeout: poll.deadlineTimeout
)),
threadId: strongSelf.chatLocation.threadId,
replyToMessageId: nil,
replyToStoryId: nil,
localGroupingKey: nil,
correlationId: nil,
bubbleUpEmojiOrStickersets: []
)
strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)])
})
}
func transformEnqueueMessages(_ messages: [EnqueueMessage]) -> [EnqueueMessage] {
let silentPosting = self.presentationInterfaceState.interfaceState.silentPosting
return transformEnqueueMessages(messages, silentPosting: silentPosting)
}
@discardableResult func dismissAllUndoControllers() -> UndoOverlayController? {
var currentOverlayController: UndoOverlayController?
self.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
currentOverlayController = controller
}
})
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
currentOverlayController = controller
}
return true
})
return currentOverlayController
}
func displayPremiumStickerTooltip(file: TelegramMediaFile, message: Message) {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
guard !premiumConfiguration.isPremiumDisabled else {
return
}
let currentOverlayController: UndoOverlayController? = self.dismissAllUndoControllers()
if let currentOverlayController = currentOverlayController {
if case .sticker = currentOverlayController.content {
return
}
currentOverlayController.dismissWithCommitAction()
}
var stickerPackReference: StickerPackReference?
for attribute in file.attributes {
if case let .Sticker(_, packReference, _) = attribute, let packReference = packReference {
stickerPackReference = packReference
break
}
}
if let stickerPackReference = stickerPackReference {
let _ = (self.context.engine.stickers.loadedStickerPack(reference: stickerPackReference, forceActualized: false)
|> deliverOnMainQueue).startStandalone(next: { [weak self] stickerPack in
if let strongSelf = self, case let .result(info, _, _) = stickerPack {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: info.title, text: strongSelf.presentationData.strings.Stickers_PremiumPackInfoText, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self, action == .undo {
let _ = strongSelf.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .default))
}
return false
}), in: .current)
}
})
}
}
func displayEmojiPackTooltip(file: TelegramMediaFile, message: Message) {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
guard !premiumConfiguration.isPremiumDisabled else {
return
}
var currentOverlayController: UndoOverlayController?
self.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
currentOverlayController = controller
}
})
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
currentOverlayController = controller
}
return true
})
if let currentOverlayController = currentOverlayController {
if case .sticker = currentOverlayController.content {
return
}
currentOverlayController.dismissWithCommitAction()
}
var stickerPackReference: StickerPackReference?
for attribute in file.attributes {
if case let .CustomEmoji(_, _, _, packReference) = attribute {
stickerPackReference = packReference
break
}
}
if let stickerPackReference = stickerPackReference {
self.presentEmojiList(references: [stickerPackReference])
/*let _ = (self.context.engine.stickers.loadedStickerPack(reference: stickerPackReference, forceActualized: false)
|> deliverOnMainQueue).startStandalone(next: { [weak self] stickerPack in
if let strongSelf = self, case let .result(info, _, _) = stickerPack {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: nil, text: strongSelf.presentationData.strings.Stickers_EmojiPackInfoText(info.title).string, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self, action == .undo {
strongSelf.presentEmojiList(references: [stickerPackReference])
}
return false
}), in: .current)
}
})*/
}
}
func displayDiceTooltip(dice: TelegramMediaDice) {
guard let _ = dice.value else {
return
}
self.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
})
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
return true
})
let value: String?
let emoji = dice.emoji.strippedEmoji
switch emoji {
case "🎲":
value = self.presentationData.strings.Conversation_Dice_u1F3B2
case "🎯":
value = self.presentationData.strings.Conversation_Dice_u1F3AF
case "🏀":
value = self.presentationData.strings.Conversation_Dice_u1F3C0
case "":
value = self.presentationData.strings.Conversation_Dice_u26BD
case "🎰":
value = self.presentationData.strings.Conversation_Dice_u1F3B0
case "🎳":
value = self.presentationData.strings.Conversation_Dice_u1F3B3
default:
let emojiHex = emoji.unicodeScalars.map({ String(format:"%02x", $0.value) }).joined().uppercased()
let key = "Conversation.Dice.u\(emojiHex)"
if let string = self.presentationData.strings.primaryComponent.dict[key] {
value = string
} else if let string = self.presentationData.strings.secondaryComponent?.dict[key] {
value = string
} else {
value = nil
}
}
if let value = value {
self.present(UndoOverlayController(presentationData: self.presentationData, content: .dice(dice: dice, context: self.context, text: value, action: canSendMessagesToChat(self.presentationInterfaceState) ? self.presentationData.strings.Conversation_SendDice : nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState), action == .undo {
strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
}
return false
}), in: .current)
}
}
func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] {
var defaultReplyMessageSubject: EngineMessageReplySubject?
switch self.chatLocation {
case .peer:
break
case let .replyThread(replyThreadMessage):
if let effectiveMessageId = replyThreadMessage.effectiveMessageId {
defaultReplyMessageSubject = EngineMessageReplySubject(messageId: effectiveMessageId, quote: nil)
}
case .customChatContents:
break
}
return messages.map { message in
var message = message
if let defaultReplyMessageSubject = defaultReplyMessageSubject {
switch message {
case let .message(text, attributes, inlineStickers, mediaReference, threadId, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets):
if replyToMessageId == nil {
message = .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: defaultReplyMessageSubject, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)
}
case .forward:
break
}
}
if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId {
switch message {
case let .message(text, attributes, inlineStickers, mediaReference, threadId, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets):
message = .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId ?? replyThreadMessage.threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)
case .forward:
break
}
}
return message.withUpdatedAttributes { attributes in
var attributes = attributes
if silentPosting || scheduleTime != nil {
for i in (0 ..< attributes.count).reversed() {
if attributes[i] is NotificationInfoMessageAttribute {
attributes.remove(at: i)
} else if let _ = scheduleTime, attributes[i] is OutgoingScheduleInfoMessageAttribute {
attributes.remove(at: i)
}
}
if silentPosting {
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
}
if let scheduleTime = scheduleTime {
attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime))
}
}
if let sendAsPeerId = self.presentationInterfaceState.currentSendAsPeerId {
if attributes.first(where: { $0 is SendAsMessageAttribute }) == nil {
attributes.append(SendAsMessageAttribute(peerId: sendAsPeerId))
}
}
if let sendMessageEffect = self.presentationInterfaceState.interfaceState.sendMessageEffect {
if attributes.first(where: { $0 is EffectMessageAttribute }) == nil {
attributes.append(EffectMessageAttribute(id: sendMessageEffect))
}
}
return attributes
}
}
}
func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) {
if case let .customChatContents(customChatContents) = self.subject {
customChatContents.enqueueMessages(messages: messages)
return
}
guard let peerId = self.chatLocation.peerId else {
return
}
var isScheduledMessages = false
if case .scheduledMessages = self.presentationInterfaceState.subject {
isScheduledMessages = true
}
if commit || !isScheduledMessages {
self.commitPurposefulAction()
let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: self.transformEnqueueMessages(messages))
|> deliverOnMainQueue).startStandalone(next: { [weak self] _ in
if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
}
})
donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId])
self.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) })
} else {
self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] time in
if let strongSelf = self {
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time), commit: true)
}
})
}
}
func enqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) {
self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: self.context, account: self.context.account, signals: signals!)
|> deliverOnMainQueue).startStrict(next: { [weak self] items in
if let strongSelf = self {
var completionImpl: (() -> Void)? = completion
var usedCorrelationId: Int64?
var mappedMessages: [EnqueueMessage] = []
var addedTransitions: [(Int64, [String], () -> Void)] = []
var groupedCorrelationIds: [Int64: Int64] = [:]
var skipAddingTransitions = false
for item in items {
var message = item.message
if message.groupingKey != nil {
if items.count > 10 {
skipAddingTransitions = true
}
} else if items.count > 3 {
skipAddingTransitions = true
}
if let uniqueId = item.uniqueId, !item.isFile && !skipAddingTransitions {
let correlationId: Int64
var addTransition = scheduleTime == nil
if let groupingKey = message.groupingKey {
if let existing = groupedCorrelationIds[groupingKey] {
correlationId = existing
addTransition = false
} else {
correlationId = Int64.random(in: 0 ..< Int64.max)
groupedCorrelationIds[groupingKey] = correlationId
}
} else {
correlationId = Int64.random(in: 0 ..< Int64.max)
}
message = message.withUpdatedCorrelationId(correlationId)
if addTransition {
addedTransitions.append((correlationId, [uniqueId], addedTransitions.isEmpty ? completion : {}))
} else {
if let index = addedTransitions.firstIndex(where: { $0.0 == correlationId }) {
var (correlationId, uniqueIds, completion) = addedTransitions[index]
uniqueIds.append(uniqueId)
addedTransitions[index] = (correlationId, uniqueIds, completion)
}
}
usedCorrelationId = correlationId
completionImpl = nil
}
if let parameters {
if let effect = parameters.effect {
message = message.withUpdatedAttributes { attributes in
var attributes = attributes
attributes.append(EffectMessageAttribute(id: effect.id))
return attributes
}
}
if parameters.textIsAboveMedia {
message = message.withUpdatedAttributes { attributes in
var attributes = attributes
attributes.append(InvertMediaMessageAttribute())
return attributes
}
}
}
mappedMessages.append(message)
}
if addedTransitions.count > 1 {
var transitions: [(Int64, ChatMessageTransitionNodeImpl.Source, () -> Void)] = []
for (correlationId, uniqueIds, initiated) in addedTransitions {
var source: ChatMessageTransitionNodeImpl.Source?
if uniqueIds.count > 1 {
source = .groupedMediaInput(ChatMessageTransitionNodeImpl.Source.GroupedMediaInput(extractSnapshots: {
return uniqueIds.compactMap({ getAnimatedTransitionSource?($0) })
}))
} else if let uniqueId = uniqueIds.first {
source = .mediaInput(ChatMessageTransitionNodeImpl.Source.MediaInput(extractSnapshot: {
return getAnimatedTransitionSource?(uniqueId)
}))
}
if let source = source {
transitions.append((correlationId, source, initiated))
}
}
strongSelf.chatDisplayNode.messageTransitionNode.add(grouped: transitions)
} else if let (correlationId, uniqueIds, initiated) = addedTransitions.first {
var source: ChatMessageTransitionNodeImpl.Source?
if uniqueIds.count > 1 {
source = .groupedMediaInput(ChatMessageTransitionNodeImpl.Source.GroupedMediaInput(extractSnapshots: {
return uniqueIds.compactMap({ getAnimatedTransitionSource?($0) })
}))
} else if let uniqueId = uniqueIds.first {
source = .mediaInput(ChatMessageTransitionNodeImpl.Source.MediaInput(extractSnapshot: {
return getAnimatedTransitionSource?(uniqueId)
}))
}
if let source = source {
strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: {
initiated()
})
}
}
if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject, let messageLimit = customChatContents.messageLimit {
if let originalHistoryView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, originalHistoryView.entries.count + mappedMessages.count > messageLimit {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_QuickReplyMediaMessageLimitReachedText(Int32(messageLimit)), actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
]), in: .window(.root))
return
}
}
let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime)
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }
})
}
completionImpl?()
}, usedCorrelationId)
strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }, media: true)
if let _ = scheduleTime {
completion()
}
}
}))
}
func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) {
if !canSendMessagesToChat(self.presentationInterfaceState) {
return
}
guard let peerId = self.chatLocation.peerId else {
return
}
var isScheduledMessages = false
if case .scheduledMessages = self.presentationInterfaceState.subject {
isScheduledMessages = true
}
let sendMessage: (Int32?) -> Void = { [weak self] scheduleTime in
guard let self else {
return
}
let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject
if self.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peerId, threadId: self.chatLocation.threadId, botId: results.botId, result: result, replyToMessageId: replyMessageSubject?.subjectModel, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) {
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
if let strongSelf = self {
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
var state = state
if resetTextInputState {
state = state.updatedInterfaceState { interfaceState in
var interfaceState = interfaceState
interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil)
interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "")))
interfaceState = interfaceState.withUpdatedComposeDisableUrlPreviews([])
return interfaceState
}
}
state = state.updatedInputMode { current in
if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil {
return .media(mode: mode, expanded: nil, focused: focused)
}
return current
}
return state
})
}
}, nil)
}
}
if isScheduledMessages {
self.presentScheduleTimePicker(style: .default, dismissByTapOutside: false, completion: { time in
sendMessage(time)
})
} else {
sendMessage(nil)
}
}
func firstLoadedMessageToListen() -> Message? {
var messageToListen: Message?
self.chatDisplayNode.historyNode.forEachMessageInCurrentHistoryView { message in
if message.flags.contains(.Incoming) && message.tags.contains(.voiceOrInstantVideo) {
for attribute in message.attributes {
if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed {
messageToListen = message
return false
}
}
}
return true
}
return messageToListen
}
var raiseToListenActivateRecordingTimer: SwiftSignalKit.Timer?
func activateRaiseGesture() {
self.raiseToListenActivateRecordingTimer?.invalidate()
self.raiseToListenActivateRecordingTimer = nil
if let messageToListen = self.firstLoadedMessageToListen() {
let _ = self.controllerInteraction?.openMessage(messageToListen, OpenMessageParams(mode: .default))
} else {
let timeout = (self.voicePlaylistDidEndTimestamp + 1.0) - CACurrentMediaTime()
self.raiseToListenActivateRecordingTimer = SwiftSignalKit.Timer(timeout: max(0.0, timeout), repeat: false, completion: { [weak self] in
self?.requestAudioRecorder(beginWithTone: true)
}, queue: .mainQueue())
self.raiseToListenActivateRecordingTimer?.start()
}
}
func deactivateRaiseGesture() {
self.raiseToListenActivateRecordingTimer?.invalidate()
self.raiseToListenActivateRecordingTimer = nil
self.dismissMediaRecorder(.pause)
}
func updateDownButtonVisibility() {
let recordingMediaMessage = self.audioRecorderValue != nil || self.videoRecorderValue != nil || self.presentationInterfaceState.interfaceState.mediaDraftState != nil
var ignoreSearchState = false
if case let .customChatContents(contents) = self.subject, case .hashTagSearch = contents.kind {
ignoreSearchState = true
}
if !ignoreSearchState, let search = self.presentationInterfaceState.search, let results = search.resultsState, results.messageIndices.count != 0 {
var resultIndex: Int?
if let currentId = results.currentId, let index = results.messageIndices.firstIndex(where: { $0.id == currentId }) {
resultIndex = index
} else {
resultIndex = nil
}
if let resultIndex {
self.chatDisplayNode.navigateButtons.directionButtonState = ChatHistoryNavigationButtons.DirectionState(
up: ChatHistoryNavigationButtons.ButtonState(isEnabled: resultIndex != 0),
down: ChatHistoryNavigationButtons.ButtonState(isEnabled: resultIndex != Int(results.totalCount) - 1 || (self.shouldDisplayDownButton && !recordingMediaMessage))
)
} else {
self.chatDisplayNode.navigateButtons.directionButtonState = ChatHistoryNavigationButtons.DirectionState(
up: ChatHistoryNavigationButtons.ButtonState(isEnabled: false),
down: ChatHistoryNavigationButtons.ButtonState(isEnabled: false)
)
}
} else {
self.chatDisplayNode.navigateButtons.directionButtonState = ChatHistoryNavigationButtons.DirectionState(
up: nil,
down: (self.shouldDisplayDownButton && !recordingMediaMessage) ? ChatHistoryNavigationButtons.ButtonState(isEnabled: true) : nil
)
}
}
func updateTextInputState(_ textInputState: ChatTextInputState) {
self.updateChatPresentationInterfaceState(interactive: false, { state in
state.updatedInterfaceState({ state in
state.withUpdatedComposeInputState(textInputState)
})
})
}
public func navigateToMessage(messageLocation: NavigateToMessageLocation, animated: Bool, forceInCurrentChat: Bool = false, dropStack: Bool = false, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil) {
let scrollPosition: ListViewScrollPosition
if case .upperBound = messageLocation {
scrollPosition = .top(0.0)
} else {
scrollPosition = .center(.bottom)
}
self.navigateToMessage(from: nil, to: messageLocation, scrollPosition: scrollPosition, rememberInStack: false, forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, animated: animated, completion: completion, customPresentProgress: customPresentProgress)
}
func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNodeImpl?, avatarNode: AvatarNode?) {
if let avatarNode = avatarHeaderNode?.avatarNode ?? avatarNode {
StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: avatarNode)
}
}
func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil, progress: Promise<Bool>? = nil) {
let _ = self.presentVoiceMessageDiscardAlert(action: {
let disposable: MetaDisposable
if let resolvePeerByNameDisposable = self.resolvePeerByNameDisposable {
disposable = resolvePeerByNameDisposable
} else {
disposable = MetaDisposable()
self.resolvePeerByNameDisposable = disposable
}
var resolveSignal = self.context.engine.peers.resolvePeerByName(name: name, ageLimit: 10)
var cancelImpl: (() -> Void)?
let presentationData = self.presentationData
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
if progress != nil {
return ActionDisposable {
}
} else {
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
resolveSignal = resolveSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = { [weak self] in
self?.resolvePeerByNameDisposable?.set(nil)
}
disposable.set((resolveSignal
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
switch result {
case .progress:
progress?.set(.single(true))
case let .result(peer):
progress?.set(.single(false))
if let peer {
var navigation = navigation
if case .default = navigation {
if case let .user(user) = peer, user.botInfo != nil {
navigation = .chat(textInputState: nil, subject: nil, peekData: nil)
}
}
self.openResolved(result: .peer(peer._asPeer(), navigation), sourceMessageId: sourceMessageId)
} else {
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Resolve_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}
}))
})
}
func openHashtag(_ hashtag: String, peerName: String?) {
let _ = self.presentVoiceMessageDiscardAlert(action: {
if self.resolvePeerByNameDisposable == nil {
self.resolvePeerByNameDisposable = MetaDisposable()
}
var resolveSignal: Signal<Peer?, NoError>
if let peerName = peerName {
resolveSignal = self.context.engine.peers.resolvePeerByName(name: peerName)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
|> mapToSignal { peer -> Signal<Peer?, NoError> in
if let peer = peer {
return .single(peer._asPeer())
} else {
return .single(nil)
}
}
} else if let peerId = self.chatLocation.peerId {
resolveSignal = self.context.account.postbox.loadedPeerWithId(peerId)
|> map(Optional.init)
} else {
resolveSignal = .single(nil)
}
var cancelImpl: (() -> Void)?
let presentationData = self.presentationData
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.25, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
resolveSignal = resolveSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = { [weak self] in
self?.resolvePeerByNameDisposable?.set(nil)
}
self.resolvePeerByNameDisposable?.set((resolveSignal
|> deliverOnMainQueue).start(next: { [weak self] peer in
if let self, !hashtag.isEmpty {
var publicPosts = false
if let peer = self.presentationInterfaceState.renderedPeer, let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info, !(channel.addressName ?? "").isEmpty {
publicPosts = true
} else if case let .customChatContents(contents) = self.subject, case let .hashTagSearch(publicPostsValue) = contents.kind {
publicPosts = publicPostsValue
}
let searchController = HashtagSearchController(context: self.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, publicPosts: publicPosts)
self.effectiveNavigationController?.pushViewController(searchController)
}
}))
})
}
func shareAccountContact() {
let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|> deliverOnMainQueue).startStandalone(next: { [weak self] accountPeer in
guard let strongSelf = self else {
return
}
guard let user = accountPeer as? TelegramUser, let phoneNumber = user.phone else {
return
}
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser else {
return
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumberConfirmation(formatPhoneNumber(context: strongSelf.context, number: phoneNumber), EnginePeer(peer).compactDisplayTitle).string))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.engine.contacts.acceptAndShareContact(peerId: peer.id)
|> deliverOnMainQueue).startStandalone(error: { _ in
guard let strongSelf = self else {
return
}
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, completed: {
guard let strongSelf = self else {
return
}
strongSelf.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .genericSuccess(strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber_StatusSuccess(EnginePeer(peer).compactDisplayTitle).string, true)), in: .window(.root))
})
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(actionSheet, in: .window(.root))
})
}
func addPeerContact() {
if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser, let peerStatusSettings = self.presentationInterfaceState.contactStatus?.peerStatusSettings, let contactData = DeviceContactExtendedData(peer: EnginePeer(peer)) {
self.present(context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: self.context), environment: ShareControllerAppEnvironment(sharedContext: self.context.sharedContext), subject: .create(peer: peer, contactData: contactData, isSharing: true, shareViaException: peerStatusSettings.contains(.addExceptionWhenAddingContact), completion: { [weak self] peer, stableId, contactData in
guard let strongSelf = self else {
return
}
if let peer = peer as? TelegramUser {
if let phone = peer.phone, !phone.isEmpty {
}
self?.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .genericSuccess(strongSelf.presentationData.strings.AddContact_StatusSuccess(EnginePeer(peer).compactDisplayTitle).string, true)), in: .window(.root))
}
}), completed: nil, cancelled: nil), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
}
func dismissPeerContactOptions() {
guard case let .peer(peerId) = self.chatLocation else {
return
}
let dismissPeerId: PeerId
if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser {
dismissPeerId = peer.id
} else {
dismissPeerId = peerId
}
self.editMessageDisposable.set((self.context.engine.peers.dismissPeerStatusOptions(peerId: dismissPeerId)
|> afterDisposed({
Queue.mainQueue().async {
}
})).startStrict())
}
func deleteChat(reportChatSpam: Bool) {
guard case let .peer(peerId) = self.chatLocation else {
return
}
self.commitPurposefulAction()
self.chatDisplayNode.historyNode.disconnect()
let _ = self.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: reportChatSpam).startStandalone()
self.effectiveNavigationController?.popToRoot(animated: true)
let _ = self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peerId, isBlocked: true).startStandalone()
}
func startBot(_ payload: String?) {
guard case let .peer(peerId) = self.chatLocation else {
return
}
let startingBot = self.startingBot
startingBot.set(true)
self.editMessageDisposable.set((self.context.engine.messages.requestStartBot(botPeerId: peerId, payload: payload) |> deliverOnMainQueue |> afterDisposed({
startingBot.set(false)
})).startStrict(completed: { [weak self] in
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedBotStartPayload(nil) })
}
}))
}
func openResolved(result: ResolvedUrl, sourceMessageId: MessageId?, progress: Promise<Bool>? = nil, forceExternal: Bool = false, concealed: Bool = false, commit: @escaping () -> Void = {}) {
let urlContext: OpenURLContext
let message = sourceMessageId.flatMap { self.chatDisplayNode.historyNode.messageInCurrentHistoryView($0) }
if let peerId = self.chatLocation.peerId {
urlContext = .chat(peerId: peerId, message: message, updatedPresentationData: self.updatedPresentationData)
} else {
urlContext = .generic
}
self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: urlContext, navigationController: self.effectiveNavigationController, forceExternal: forceExternal, openPeer: { [weak self] peerId, navigation in
guard let strongSelf = self else {
return
}
let dismissWebAppControllers: () -> Void = {
}
switch navigation {
case let .chat(textInputState, subject, peekData):
dismissWebAppControllers()
if case .peer(peerId.id) = strongSelf.chatLocation {
if let subject = subject, case let .message(messageSubject, _, timecode) = subject {
if case let .id(messageId) = messageSubject {
strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil)))
}
} else {
self?.playShakeAnimation()
}
} else if let navigationController = strongSelf.effectiveNavigationController {
if case let .channel(channel) = peerId, channel.flags.contains(.isForum) {
strongSelf.context.sharedContext.navigateToForumChannel(context: strongSelf.context, peerId: peerId.id, navigationController: navigationController)
} else {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: subject, updateTextInputState: !peerId.id.isGroupOrChannel ? textInputState : nil, keepStack: .always, peekData: peekData))
}
}
commit()
case .info:
dismissWebAppControllers()
strongSelf.navigationActionDisposable.set((strongSelf.context.account.postbox.loadedPeerWithId(peerId.id)
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] peer in
if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil {
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
strongSelf.effectiveNavigationController?.pushViewController(infoController)
}
}
}))
commit()
case let .withBotStartPayload(startPayload):
dismissWebAppControllers()
if case .peer(peerId.id) = strongSelf.chatLocation {
strongSelf.startBot(startPayload.payload)
} else if let navigationController = strongSelf.effectiveNavigationController {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), botStart: startPayload, keepStack: .always))
}
commit()
case let .withAttachBot(attachBotStart):
dismissWebAppControllers()
if let navigationController = strongSelf.effectiveNavigationController {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), attachBotStart: attachBotStart))
}
commit()
case let .withBotApp(botAppStart):
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId.id))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
if let strongSelf = self, let peer {
strongSelf.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact, concealed: concealed, commit: {
dismissWebAppControllers()
commit()
})
}
})
default:
break
}
}, sendFile: nil, sendSticker: { [weak self] f, sourceView, sourceRect in
return self?.interfaceInteraction?.sendSticker(f, true, sourceView, sourceRect, nil, []) ?? false
}, sendEmoji: { [weak self] text, attribute in
guard let self, canSendMessagesToChat(self.presentationInterfaceState) else {
return
}
self.controllerInteraction?.sendEmoji(text, attribute, false)
},
requestMessageActionUrlAuth: { [weak self] subject in
if case let .url(url) = subject {
self?.controllerInteraction?.requestMessageActionUrlAuth(url, subject)
}
}, joinVoiceChat: { [weak self] peerId, invite, call in
self?.joinGroupCall(peerId: peerId, invite: invite, activeCall: EngineGroupCallDescription(call))
}, present: { [weak self] c, a in
if c is UndoOverlayController {
self?.present(c, in: .current)
} else {
self?.present(c, in: .window(.root), with: a)
}
}, dismissInput: { [weak self] in
self?.chatDisplayNode.dismissInput()
}, contentContext: nil, progress: progress, completion: nil)
}
func openUrl(_ url: String, concealed: Bool, forceExternal: Bool = false, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, message: Message? = nil, allowInlineWebpageResolution: Bool = false, progress: Promise<Bool>? = nil, commit: @escaping () -> Void = {}) {
self.commitPurposefulAction()
if allowInlineWebpageResolution, let message, let webpage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.url == url {
if content.instantPage != nil {
if let navigationController = self.navigationController as? NavigationController {
switch instantPageType(of: content) {
case .album:
break
default:
progress?.set(.single(false))
self.context.sharedContext.openChatInstantPage(context: self.context, message: message, sourcePeerType: nil, navigationController: navigationController)
return
}
}
} else if content.file == nil, (content.image == nil || content.isMediaLargeByDefault == true || content.isMediaLargeByDefault == nil), let embedUrl = content.embedUrl, !embedUrl.isEmpty {
progress?.set(.single(false))
if let controllerInteraction = self.controllerInteraction {
if controllerInteraction.openMessage(message, OpenMessageParams(mode: .default)) {
return
}
}
}
}
let _ = self.presentVoiceMessageDiscardAlert(action: { [weak self] in
guard let self else {
return
}
let disposable = openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in
self?.present(c, in: .window(.root))
}, openResolved: { [weak self] resolved in
self?.openResolved(result: resolved, sourceMessageId: message?.id, progress: progress, forceExternal: forceExternal, concealed: concealed, commit: commit)
}, progress: progress)
self.navigationActionDisposable.set(disposable)
}, performAction: true)
}
func openUrlIn(_ url: String) {
let actionSheet = OpenInActionSheetController(context: self.context, updatedPresentationData: self.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in
if let strongSelf = self, let navigationController = strongSelf.effectiveNavigationController {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: {
self?.chatDisplayNode.dismissInput()
})
}
})
self.chatDisplayNode.dismissInput()
self.present(actionSheet, in: .window(.root))
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String])
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
if !canSendMessagesToChat(self.presentationInterfaceState) {
return UIDropProposal(operation: .cancel)
}
//let dropLocation = session.location(in: self.chatDisplayNode.view)
self.chatDisplayNode.updateDropInteraction(isActive: true)
let operation: UIDropOperation
operation = .copy
return UIDropProposal(operation: operation)
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in
guard let strongSelf = self, !imageItems.isEmpty else {
return
}
let images = imageItems as! [UIImage]
strongSelf.chatDisplayNode.updateDropInteraction(isActive: false)
if images.count == 1, let image = images.first {
let maxSide = max(image.size.width, image.size.height)
if maxSide.isZero {
return
}
let aspectRatio = min(image.size.width, image.size.height) / maxSide
if (imageHasTransparency(image) && aspectRatio > 0.2) {
strongSelf.enqueueStickerImage(image, isMemoji: false)
return
}
}
strongSelf.chatDisplayNode.updateDropInteraction(isActive: false)
strongSelf.displayPasteMenu(images.map { .image($0) })
}
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) {
self.chatDisplayNode.updateDropInteraction(isActive: false)
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) {
self.chatDisplayNode.updateDropInteraction(isActive: false)
}
public func beginMessageSearch(_ query: String) {
self.interfaceInteraction?.beginMessageSearch(.everything, query)
}
public func beginReportSelection(reason: ReportReason) {
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedReportReason(reason).updatedInterfaceState { $0.withUpdatedSelectedMessages([]) } })
}
func displayMediaRecordingTooltip() {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
if self.birthdayTooltipController != nil {
return
}
let rect: CGRect? = self.chatDisplayNode.frameForInputActionButton()
let updatedMode: ChatTextInputMediaRecordingButtonMode = self.presentationInterfaceState.interfaceState.mediaRecordingMode
let text: String
var canSwitch = true
if let channel = peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
canSwitch = false
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if channel.hasBannedPermission(.banSendInstantVideos) == nil {
canSwitch = false
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if channel.hasBannedPermission(.banSendVoice) == nil {
canSwitch = false
}
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
canSwitch = false
} else if group.hasBannedPermission(.banSendVoice) {
if !group.hasBannedPermission(.banSendInstantVideos) {
canSwitch = false
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if !group.hasBannedPermission(.banSendVoice) {
canSwitch = false
}
}
}
if updatedMode == .audio {
if canSwitch {
text = self.presentationData.strings.Conversation_HoldForAudio
} else {
text = self.presentationData.strings.Conversation_HoldForAudioOnly
}
} else {
if canSwitch {
text = self.presentationData.strings.Conversation_HoldForVideo
} else {
text = self.presentationData.strings.Conversation_HoldForVideoOnly
}
}
self.silentPostTooltipController?.dismiss()
if let tooltipController = self.mediaRecordingModeTooltipController {
tooltipController.updateContent(.text(text), animated: true, extendTimer: true)
} else if let rect = rect {
let tooltipController = TooltipController(content: .text(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, padding: 2.0)
self.mediaRecordingModeTooltipController = tooltipController
tooltipController.dismissed = { [weak self, weak tooltipController] _ in
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRecordingModeTooltipController === tooltipController {
strongSelf.mediaRecordingModeTooltipController = nil
}
}
self.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let strongSelf = self {
return (strongSelf.chatDisplayNode, rect)
}
return nil
}))
}
}
func displaySendWhenOnlineTooltip() {
guard let rect = self.chatDisplayNode.frameForInputActionButton(), self.effectiveNavigationController?.topViewController === self, let peerId = self.chatLocation.peerId else {
return
}
let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string
guard !inputText.isEmpty else {
return
}
self.sendingOptionsTooltipController?.dismiss()
let _ = (ApplicationSpecificNotice.getSendWhenOnlineTip(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).startStandalone(next: { [weak self] counter in
if let strongSelf = self, counter < 3 {
let _ = (strongSelf.context.account.viewTracker.peerView(peerId)
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in
guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else {
return
}
var sendWhenOnlineAvailable = false
if peer.id != strongSelf.context.account.peerId, let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status, until != .max {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let (_, _, _, hours, _) = getDateTimeComponents(timestamp: currentTime)
if currentTime > until + 60 * 30 && hours >= 0 && hours <= 8 {
sendWhenOnlineAvailable = true
}
}
if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 {
sendWhenOnlineAvailable = false
}
if sendWhenOnlineAvailable {
let _ = ApplicationSpecificNotice.incrementSendWhenOnlineTip(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone()
let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.Conversation_SendWhenOnlineTooltip), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, timeout: 3.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true, padding: 2.0)
strongSelf.sendingOptionsTooltipController = tooltipController
tooltipController.dismissed = { [weak self, weak tooltipController] _ in
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.sendingOptionsTooltipController === tooltipController {
strongSelf.sendingOptionsTooltipController = nil
}
}
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let strongSelf = self {
return (strongSelf.chatDisplayNode, rect)
}
return nil
}))
}
})
}
})
}
func displaySendingOptionsTooltip() {
guard let rect = self.chatDisplayNode.frameForInputActionButton(), self.effectiveNavigationController?.topViewController === self else {
return
}
self.sendingOptionsTooltipController?.dismiss()
let tooltipController = TooltipController(content: .text(self.presentationData.strings.Conversation_SendingOptionsTooltip), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 3.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true, padding: 2.0)
self.sendingOptionsTooltipController = tooltipController
tooltipController.dismissed = { [weak self, weak tooltipController] _ in
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.sendingOptionsTooltipController === tooltipController {
strongSelf.sendingOptionsTooltipController = nil
}
}
self.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let strongSelf = self {
return (strongSelf.chatDisplayNode, rect)
}
return nil
}))
}
func displayEmojiTooltip() {
guard let rect = self.chatDisplayNode.frameForEmojiButton(), self.effectiveNavigationController?.topViewController === self else {
return
}
self.emojiTooltipController?.dismiss()
let tooltipController = TooltipController(content: .text(self.presentationData.strings.Conversation_EmojiTooltip), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 3.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true, padding: 2.0)
self.emojiTooltipController = tooltipController
tooltipController.dismissed = { [weak self, weak tooltipController] _ in
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.emojiTooltipController === tooltipController {
strongSelf.emojiTooltipController = nil
}
}
self.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let strongSelf = self {
return (strongSelf.chatDisplayNode, rect.offsetBy(dx: 0.0, dy: -3.0))
}
return nil
}))
}
func displayGroupEmojiTooltip() {
guard let rect = self.chatDisplayNode.frameForEmojiButton(), self.effectiveNavigationController?.topViewController === self else {
return
}
guard let peerId = self.chatLocation.peerId, let emojiPack = (self.peerView?.cachedData as? CachedChannelData)?.emojiPack, let thumbnailFileId = emojiPack.thumbnailFileId else {
return
}
let _ = (ApplicationSpecificNotice.groupEmojiPackSuggestion(accountManager: self.context.sharedContext.accountManager, peerId: peerId)
|> deliverOnMainQueue).start(next: { [weak self] counter in
guard let self, counter == 0 else {
return
}
let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [thumbnailFileId])
|> deliverOnMainQueue).start(next: { [weak self] files in
guard let self, let emojiFile = files.values.first else {
return
}
let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)
let boldTextFont = Font.bold(self.presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)
let textColor = UIColor.white
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in
return nil
})
let text = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(self.presentationData.strings.Chat_GroupEmojiTooltip(emojiPack.title).string, attributes: markdownAttributes))
let range = (text.string as NSString).range(of: "#")
if range.location != NSNotFound {
text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiFile.fileId.id, file: emojiFile), range: range)
}
let tooltipScreen = TooltipScreen(
context: self.context,
account: self.context.account,
sharedContext: self.context.sharedContext,
text: .attributedString(text: text),
location: .point(rect.offsetBy(dx: 0.0, dy: -3.0), .bottom),
displayDuration: .default,
cornerRadius: 10.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.present(tooltipScreen, in: .current)
self.emojiPackTooltipController = tooltipScreen
let _ = ApplicationSpecificNotice.incrementGroupEmojiPackSuggestion(accountManager: self.context.sharedContext.accountManager, peerId: peerId).startStandalone()
})
})
}
private var didDisplayBirthdayTooltip = false
func displayBirthdayTooltip() {
guard !self.didDisplayBirthdayTooltip else {
return
}
if let birthday = (self.peerView?.cachedData as? CachedUserData)?.birthday {
PeerInfoScreenImpl.preloadBirthdayAnimations(context: self.context, birthday: birthday)
}
guard let rect = self.chatDisplayNode.frameForGiftButton(), self.effectiveNavigationController?.topViewController === self, let peer = self.presentationInterfaceState.renderedPeer?.peer.flatMap({ EnginePeer($0) }) else {
return
}
self.didDisplayBirthdayTooltip = true
let _ = (ApplicationSpecificNotice.dismissedBirthdayPremiumGiftTip(accountManager: self.context.sharedContext.accountManager, peerId: peer.id)
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] timestamp in
if let self {
let currentTime = Int32(Date().timeIntervalSince1970)
if let timestamp, currentTime < timestamp + 60 * 60 * 24 {
return
}
let peerName = peer.compactDisplayTitle
let text = self.presentationData.strings.Chat_BirthdayTooltip(peerName, peerName).string
let tooltipScreen = TooltipScreen(
context: self.context,
account: self.context.account,
sharedContext: self.context.sharedContext,
text: .markdown(text: text),
location: .point(rect.offsetBy(dx: 0.0, dy: -3.0), .bottom),
displayDuration: .custom(6.0),
cornerRadius: 10.0,
shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
}
)
self.birthdayTooltipController = tooltipScreen
Queue.mainQueue().after(0.35) {
self.present(tooltipScreen, in: .current)
}
let _ = ApplicationSpecificNotice.incrementDismissedBirthdayPremiumGiftTip(accountManager: self.context.sharedContext.accountManager, peerId: peer.id, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone()
}
})
}
func displayChecksTooltip() {
self.checksTooltipController?.dismiss()
var latestNode: (Int32, ASDisplayNode)?
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, let statusNode = itemNode.getStatusNode() {
if !item.content.effectivelyIncoming(self.context.account.peerId) {
if let (latestTimestamp, _) = latestNode {
if item.message.timestamp > latestTimestamp {
latestNode = (item.message.timestamp, statusNode)
}
} else {
latestNode = (item.message.timestamp, statusNode)
}
}
}
}
if let (_, latestStatusNode) = latestNode {
let bounds = latestStatusNode.view.convert(latestStatusNode.view.bounds, to: self.chatDisplayNode.view)
let location = CGPoint(x: bounds.maxX - 7.0, y: bounds.minY - 11.0)
let contentNode = ChatStatusChecksTooltipContentNode(presentationData: self.presentationData)
let tooltipController = TooltipController(content: .custom(contentNode), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 3.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true)
self.checksTooltipController = tooltipController
tooltipController.dismissed = { [weak self, weak tooltipController] _ in
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.checksTooltipController === tooltipController {
strongSelf.checksTooltipController = nil
}
}
self.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let strongSelf = self {
return (strongSelf.chatDisplayNode, CGRect(origin: location, size: CGSize()))
}
return nil
}))
}
}
func dismissAllTooltips() {
self.emojiTooltipController?.dismiss()
self.sendingOptionsTooltipController?.dismiss()
self.searchResultsTooltipController?.dismiss()
self.messageTooltipController?.dismiss()
self.videoUnmuteTooltipController?.dismiss()
self.silentPostTooltipController?.dismiss()
self.mediaRecordingModeTooltipController?.dismiss()
self.mediaRestrictedTooltipController?.dismiss()
self.checksTooltipController?.dismiss()
self.copyProtectionTooltipController?.dismiss()
self.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
})
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
if let controller = controller as? TooltipScreen, !controller.alwaysVisible {
controller.dismiss()
}
return true
})
}
func commitPurposefulAction() {
if let purposefulAction = self.purposefulAction {
self.purposefulAction = nil
purposefulAction()
}
}
public override var keyShortcuts: [KeyShortcut] {
return self.keyShortcutsInternal
}
public override func joinGroupCall(peerId: PeerId, invite: String?, activeCall: EngineGroupCallDescription) {
let proceed = {
super.joinGroupCall(peerId: peerId, invite: invite, activeCall: activeCall)
}
let _ = self.presentVoiceMessageDiscardAlert(action: {
proceed()
})
}
public func getTransitionInfo(messageId: MessageId, media: Media) -> ((UIView) -> Void, ASDisplayNode, () -> (UIView?, UIView?))? {
var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if let result = itemNode.transitionNode(id: messageId, media: media, adjustRect: false) {
selectedNode = result
}
}
}
if let (node, _, get) = selectedNode {
return ({ [weak self] view in
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view)
}, node, get)
} else {
return nil
}
}
public func activateInput(type: ChatControllerActivateInput) {
if self.didAppear {
switch type {
case .text:
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
return state.updatedInputMode({ _ in
switch type {
case .text:
return .text
case .entityInput:
return .media(mode: .other, expanded: nil, focused: false)
}
})
})
case .entityInput:
self.chatDisplayNode.openStickers(beginWithEmoji: true)
}
} else {
self.scheduledActivateInput = type
}
}
func clearInputText() {
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
if !state.interfaceState.effectiveInputState.inputText.string.isEmpty {
return state.updatedInterfaceState { interfaceState in
let effectiveInputState = ChatTextInputState(inputText: NSAttributedString(string: ""))
return interfaceState.withUpdatedEffectiveInputState(effectiveInputState)
}
} else {
return state
}
})
}
func updateReminderActivity() {
if self.isReminderActivityEnabled && false {
if #available(iOS 9.0, *) {
if self.reminderActivity == nil, case let .peer(peerId) = self.chatLocation, let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer {
let reminderActivity = NSUserActivity(activityType: "RemindAboutChatIntent")
self.reminderActivity = reminderActivity
if peer is TelegramGroup {
reminderActivity.title = self.presentationData.strings.Activity_RemindAboutGroup(EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)).string
} else if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info {
reminderActivity.title = self.presentationData.strings.Activity_RemindAboutChannel(EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)).string
} else {
reminderActivity.title = self.presentationData.strings.Activity_RemindAboutGroup(EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)).string
}
} else {
reminderActivity.title = self.presentationData.strings.Activity_RemindAboutUser(EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)).string
}
reminderActivity.userInfo = ["peerId": peerId.toInt64(), "peerTitle": EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)]
reminderActivity.isEligibleForHandoff = true
reminderActivity.becomeCurrent()
}
}
} else if let reminderActivity = self.reminderActivity {
self.reminderActivity = nil
reminderActivity.invalidate()
}
}
func updateSlowmodeStatus() {
if let slowmodeState = self.presentationInterfaceState.slowmodeState, case let .timestamp(slowmodeActiveUntilTimestamp) = slowmodeState.variant {
let timestamp = Int32(Date().timeIntervalSince1970)
let remainingTime = max(0, slowmodeActiveUntilTimestamp - timestamp)
if remainingTime == 0 {
self.updateSlowmodeStatusTimerValue = nil
self.updateSlowmodeStatusDisposable.set(nil)
self.updateChatPresentationInterfaceState(interactive: false, {
$0.updatedSlowmodeState(nil)
})
} else {
if self.updateSlowmodeStatusTimerValue != slowmodeActiveUntilTimestamp {
self.updateSlowmodeStatusTimerValue = slowmodeActiveUntilTimestamp
self.updateSlowmodeStatusDisposable.set((Signal<Never, NoError>.complete()
|> suspendAwareDelay(Double(remainingTime), granularity: 1.0, queue: .mainQueue())
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateSlowmodeStatusTimerValue = nil
strongSelf.updateSlowmodeStatus()
}))
}
}
} else if let _ = self.updateSlowmodeStatusTimerValue {
self.updateSlowmodeStatusTimerValue = nil
self.updateSlowmodeStatusDisposable.set(nil)
}
}
func openScheduledMessages() {
guard let navigationController = self.effectiveNavigationController, navigationController.topViewController == self else {
return
}
var mappedChatLocation = self.chatLocation
if case let .replyThread(message) = self.chatLocation, message.peerId == self.context.account.peerId {
mappedChatLocation = .peer(id: self.context.account.peerId)
}
let controller = ChatControllerImpl(context: self.context, chatLocation: mappedChatLocation, subject: .scheduledMessages)
controller.navigationPresentation = .modal
navigationController.pushViewController(controller)
}
func openPinnedMessages(at messageId: MessageId?) {
let _ = self.presentVoiceMessageDiscardAlert(action: { [weak self] in
guard let self, let navigationController = self.effectiveNavigationController, navigationController.topViewController == self else {
return
}
let controller = ChatControllerImpl(context: self.context, chatLocation: self.chatLocation, subject: .pinnedMessages(id: messageId))
controller.navigationPresentation = .modal
controller.updatedClosedPinnedMessageId = { [weak self] pinnedMessageId in
guard let strongSelf = self else {
return
}
strongSelf.performUpdatedClosedPinnedMessageId(pinnedMessageId: pinnedMessageId)
}
controller.requestedUnpinAllMessages = { [weak self] count, pinnedMessageId in
guard let strongSelf = self else {
return
}
strongSelf.performRequestedUnpinAllMessages(count: count, pinnedMessageId: pinnedMessageId)
}
navigationController.pushViewController(controller)
})
}
func performUpdatedClosedPinnedMessageId(pinnedMessageId: MessageId) {
let previousClosedPinnedMessageId = self.presentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in
var value = value
value.closedPinnedMessageId = pinnedMessageId
return value
}) })
})
self.present(
UndoOverlayController(
presentationData: self.presentationData,
content: .messagesUnpinned(
title: self.presentationData.strings.Chat_PinnedMessagesHiddenTitle,
text: self.presentationData.strings.Chat_PinnedMessagesHiddenText,
undo: true,
isHidden: true
),
elevatedLayout: false,
action: { [weak self] action in
guard let strongSelf = self else {
return true
}
switch action {
case .commit:
break
case .undo:
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in
var value = value
value.closedPinnedMessageId = previousClosedPinnedMessageId
return value
}) })
})
default:
break
}
return true
}
),
in: .current
)
}
func performRequestedUnpinAllMessages(count: Int, pinnedMessageId: MessageId) {
guard let peerId = self.chatLocation.peerId else {
return
}
self.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = true
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedPendingUnpinnedAllMessages(true)
})
self.present(
UndoOverlayController(
presentationData: self.presentationData,
content: .messagesUnpinned(
title: self.presentationData.strings.Chat_MessagesUnpinned(Int32(count)),
text: "",
undo: true,
isHidden: false
),
elevatedLayout: false,
action: { [weak self] action in
guard let strongSelf = self else {
return true
}
switch action {
case .commit:
let _ = (strongSelf.context.engine.messages.requestUnpinAllMessages(peerId: peerId, threadId: strongSelf.chatLocation.threadId)
|> deliverOnMainQueue).startStandalone(error: { _ in
}, completed: {
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedPendingUnpinnedAllMessages(false)
})
})
case .undo:
strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedPendingUnpinnedAllMessages(false)
})
default:
break
}
return true
}
),
in: .current
)
}
func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
guard let peerId = self.chatLocation.peerId else {
return
}
let _ = (self.context.account.viewTracker.peerView(peerId)
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in
guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else {
return
}
var sendWhenOnlineAvailable = false
if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case .present = presence.status {
sendWhenOnlineAvailable = true
}
if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 {
sendWhenOnlineAvailable = false
}
let mode: ChatScheduleTimeControllerMode
if peerId == strongSelf.context.account.peerId {
mode = .reminders
} else {
mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable)
}
let controller = ChatScheduleTimeController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, mode: mode, style: style, currentTime: selectedTime, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, dismissByTapOutside: dismissByTapOutside, completion: { time in
completion(time)
})
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(controller, in: .window(.root))
})
}
func presentTimerPicker(style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
guard case .peer = self.chatLocation else {
return
}
let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: style, currentTime: selectedTime, dismissByTapOutside: dismissByTapOutside, completion: { time in
completion(time)
})
self.chatDisplayNode.dismissInput()
self.present(controller, in: .window(.root))
}
func presentVoiceMessageDiscardAlert(action: @escaping () -> Void = {}, alertAction: (() -> Void)? = nil, delay: Bool = false, performAction: Bool = true) -> Bool {
if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState {
alertAction?()
Queue.mainQueue().after(delay ? 0.2 : 0.0) {
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Conversation_DiscardVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_DiscardVoiceMessageAction, action: { [weak self] in
self?.stopMediaRecorder()
Queue.mainQueue().after(0.1) {
action()
}
})]), in: .window(.root))
}
return true
} else if performAction {
action()
}
return false
}
func presentRecordedVoiceMessageDiscardAlert(action: @escaping () -> Void = {}, alertAction: (() -> Void)? = nil, delay: Bool = false, performAction: Bool = true) -> Bool {
if let _ = self.presentationInterfaceState.interfaceState.mediaDraftState {
alertAction?()
Queue.mainQueue().after(delay ? 0.2 : 0.0) {
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageAction, action: { [weak self] in
self?.stopMediaRecorder()
Queue.mainQueue().after(0.1) {
action()
}
})]), in: .window(.root))
}
return true
} else if performAction {
action()
}
return false
}
func presentAutoremoveSetup() {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: .default, mode: .autoremove, currentTime: self.presentationInterfaceState.autoremoveTimeout, dismissByTapOutside: true, completion: { [weak self] value in
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: peer.id, timeout: value == 0 ? nil : value)
|> deliverOnMainQueue).startStandalone(completed: {
guard let strongSelf = self else {
return
}
var isOn: Bool = true
var text: String?
if value != 0 {
text = strongSelf.presentationData.strings.Conversation_AutoremoveChanged("\(timeIntervalString(strings: strongSelf.presentationData.strings, value: value))").string
} else {
isOn = false
text = strongSelf.presentationData.strings.Conversation_AutoremoveOff
}
if let text = text {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
}
})
})
self.chatDisplayNode.dismissInput()
self.present(controller, in: .window(.root))
}
func presentChatRequestAdminInfo() {
if let requestChatTitle = self.presentationInterfaceState.contactStatus?.peerStatusSettings?.requestChatTitle, let requestDate = self.presentationInterfaceState.contactStatus?.peerStatusSettings?.requestChatDate {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
var items: [ActionSheetItem] = []
let text = presentationData.strings.Conversation_InviteRequestInfo(requestChatTitle, stringForDate(timestamp: requestDate, strings: presentationData.strings))
items.append(ActionSheetTextItem(title: text.string))
items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_InviteRequestInfoConfirm, color: .accent, action: { [weak self, weak controller] in
controller?.dismissAnimated()
self?.interfaceInteraction?.dismissReportPeer()
}))
controller.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in
controller?.dismissAnimated()
})
])])
self.chatDisplayNode.dismissInput()
self.present(controller, in: .window(.root))
}
}
var crossfading = false
func presentCrossfadeSnapshot() {
guard !self.crossfading, let snapshotView = self.view.snapshotView(afterScreenUpdates: false) else {
return
}
self.crossfading = true
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak self, weak snapshotView] _ in
self?.crossfading = false
snapshotView?.removeFromSuperview()
})
}
public func hintPlayNextOutgoingGift() {
self.controllerInteraction?.playNextOutgoingGift = true
}
var effectiveNavigationController: NavigationController? {
if let navigationController = self.navigationController as? NavigationController {
return navigationController
} else if case let .inline(navigationController) = self.presentationInterfaceState.mode {
return navigationController
} else if case let .overlay(navigationController) = self.presentationInterfaceState.mode {
return navigationController
} else {
if let navigationController = self.customNavigationController {
return navigationController
}
return nil
}
}
public func activateSearch(domain: ChatSearchDomain = .everything, query: String = "") {
self.focusOnSearchAfterAppearance = (domain, query)
self.interfaceInteraction?.beginMessageSearch(domain, query)
}
override public func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) {
//self.chatDisplayNode.updateEmbeddedTitlePeekContent(content: content)
}
override public func acceptPossibleControllerDropContent(content: NavigationControllerDropContent) -> Bool {
//return self.chatDisplayNode.acceptEmbeddedTitlePeekContent(content: content)
return false
}
public var isSendButtonVisible: Bool {
if self.presentationInterfaceState.interfaceState.editMessage != nil || self.presentationInterfaceState.interfaceState.forwardMessageIds != nil || self.presentationInterfaceState.interfaceState.composeInputState.inputText.string.count > 0 {
return true
} else {
return false
}
}
public func playShakeAnimation() {
if self.shakeFeedback == nil {
self.shakeFeedback = HapticFeedback()
}
self.shakeFeedback?.error()
self.chatDisplayNode.historyNodeContainer.layer.addShakeAnimation(amplitude: -6.0, decay: true)
}
public func updatePushedTransition(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) {
if !transition.isAnimated {
self.chatDisplayNode.historyNodeContainer.layer.removeAllAnimations()
}
let scale: CGFloat = 1.0 - 0.06 * fraction
transition.updateTransformScale(node: self.chatDisplayNode.historyNodeContainer, scale: scale)
}
func restrictedSendingContentsText() -> String {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return self.presentationData.strings.Chat_SendNotAllowedText
}
var itemList: [String] = []
let order: [TelegramChatBannedRightsFlags] = [
.banSendText,
.banSendPhotos,
.banSendVideos,
.banSendVoice,
.banSendInstantVideos,
.banSendFiles,
.banSendMusic,
.banSendStickers
]
for right in order {
if let channel = peer as? TelegramChannel {
if channel.hasBannedPermission(right) != nil {
continue
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(right) {
continue
}
}
var title: String?
switch right {
case .banSendText:
title = self.presentationData.strings.Chat_SendAllowedContentTypeText
case .banSendPhotos:
title = self.presentationData.strings.Chat_SendAllowedContentTypePhoto
case .banSendVideos:
title = self.presentationData.strings.Chat_SendAllowedContentTypeVideo
case .banSendVoice:
title = self.presentationData.strings.Chat_SendAllowedContentTypeVoiceMessage
case .banSendInstantVideos:
title = self.presentationData.strings.Chat_SendAllowedContentTypeVideoMessage
case .banSendFiles:
title = self.presentationData.strings.Chat_SendAllowedContentTypeFile
case .banSendMusic:
title = self.presentationData.strings.Chat_SendAllowedContentTypeMusic
case .banSendStickers:
title = self.presentationData.strings.Chat_SendAllowedContentTypeSticker
default:
break
}
if let title {
itemList.append(title)
}
}
if itemList.isEmpty {
return self.presentationData.strings.Chat_SendNotAllowedText
}
var itemListString = ""
if #available(iOS 13.0, *) {
let listFormatter = ListFormatter()
listFormatter.locale = localeWithStrings(presentationData.strings)
if let value = listFormatter.string(from: itemList) {
itemListString = value
}
}
if itemListString.isEmpty {
for i in 0 ..< itemList.count {
if i != 0 {
itemListString.append(", ")
}
itemListString.append(itemList[i])
}
}
return self.presentationData.strings.Chat_SendAllowedContentText(itemListString).string
}
func updateNextChannelToReadVisibility() {
self.chatDisplayNode.historyNode.offerNextChannelToRead = self.offerNextChannelToRead && self.presentationInterfaceState.interfaceState.selectionState == nil
}
func displayGiveawayStatusInfo(messageId: EngineMessage.Id, giveawayInfo: PremiumGiveawayInfo) {
presentGiveawayInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, messageId: messageId, giveawayInfo: giveawayInfo, present: { [weak self] c in
guard let self else {
return
}
self.present(c, in: .window(.root))
}, openLink: { [weak self] slug in
guard let self else {
return
}
self.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: messageId)
})
}
public func transferScrollingVelocity(_ velocity: CGFloat) {
self.chatDisplayNode.historyNode.transferVelocity(velocity)
}
public func performScrollToTop() -> Bool {
let offset = self.chatDisplayNode.historyNode.visibleContentOffset()
switch offset {
case let .known(value) where value <= CGFloat.ulpOfOne:
return false
default:
self.chatDisplayNode.historyNode.scrollToEndOfHistory()
return true
}
}
}