import Foundation import UIKit import Postbox import SwiftSignalKit import Display import AsyncDisplayKit import TelegramCore import SafariServices import MobileCoreServices public enum ChatControllerPeekActions { case standard case remove(() -> Void) } public enum ChatControllerPresentationMode: Equatable { case standard(previewing: Bool) case overlay case inline } public final class ChatControllerOverlayPresentationData { public let expandData: (ASDisplayNode?, () -> Void) public init(expandData: (ASDisplayNode?, () -> Void)) { self.expandData = expandData } } private enum ChatLocationInfoData { case peer(Promise) case group(Promise) } private enum ChatRecordingActivity { case voice case instantVideo case none } public enum NavigateToMessageLocation { case id(MessageId) case index(MessageIndex) var messageId: MessageId { switch self { case let .id(id): return id case let .index(index): return index.id } } } private func isTopmostChatController(_ controller: ChatController) -> Bool { if let _ = controller.navigationController { var hasOther = false controller.window?.forEachController({ c in if c is ChatController { hasOther = true } }) if hasOther { return false } } return true } let ChatControllerCount = Atomic(value: 0) public final class ChatController: TelegramController, KeyShortcutResponder, UIDropInteractionDelegate { private var validLayout: ContainerViewLayout? public var peekActions: ChatControllerPeekActions = .standard private var didSetup3dTouch: Bool = false private let account: Account public let chatLocation: ChatLocation private let messageId: MessageId? private let botStart: ChatControllerInitialBotStart? private let peerDisposable = MetaDisposable() private let navigationActionDisposable = MetaDisposable() private var networkStateDisposable: Disposable? private let messageIndexDisposable = MetaDisposable() private let _chatLocationInfoReady = Promise() private var didSetChatLocationInfoReady = false private let chatLocationInfoData: ChatLocationInfoData private var presentationInterfaceState: ChatPresentationInterfaceState private var chatTitleView: ChatTitleView? private var leftNavigationButton: ChatNavigationButton? private var rightNavigationButton: ChatNavigationButton? private var chatInfoNavigationButton: ChatNavigationButton? private var peerView: PeerView? private var historyStateDisposable: Disposable? private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private let temporaryHiddenGalleryMediaDisposable = MetaDisposable() private var controllerInteraction: ChatControllerInteraction? private var interfaceInteraction: ChatPanelInterfaceInteraction? private let messageContextDisposable = MetaDisposable() private let controllerNavigationDisposable = MetaDisposable() private let sentMessageEventsDisposable = MetaDisposable() private let failedMessageEventsDisposable = MetaDisposable() private let messageActionCallbackDisposable = MetaDisposable() private let editMessageDisposable = MetaDisposable() private let enqueueMediaMessageDisposable = MetaDisposable() private var resolvePeerByNameDisposable: MetaDisposable? private var shareStatusDisposable: MetaDisposable? private let editingMessage = ValuePromise(nil, ignoreRepeated: true) private let startingBot = ValuePromise(false, ignoreRepeated: true) private let unblockingPeer = ValuePromise(false, ignoreRepeated: true) private let searching = ValuePromise(false, ignoreRepeated: true) private let loadingMessage = ValuePromise(false, ignoreRepeated: true) private let botCallbackAlertMessage = Promise(nil) private var botCallbackAlertMessageDisposable: Disposable? private var resolveUrlDisposable: MetaDisposable? private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:] private var searchQuerySuggestionState: (ChatPresentationInputQuery?, Disposable)? private var urlPreviewQueryState: (String?, Disposable)? private var editingUrlPreviewQueryState: (String?, Disposable)? private var searchState: ChatSearchState? private var recordingModeFeedback: HapticFeedback? private var recorderFeedback: HapticFeedback? private var audioRecorderValue: ManagedAudioRecorder? private var audioRecorder = Promise() private var audioRecorderDisposable: Disposable? private var videoRecorderValue: InstantVideoController? private var tempVideoRecorderValue: InstantVideoController? private var videoRecorder = Promise() private var videoRecorderDisposable: Disposable? private var buttonKeyboardMessageDisposable: Disposable? private var cachedDataDisposable: Disposable? private var chatUnreadCountDisposable: Disposable? private var chatUnreadMentionCountDisposable: Disposable? private var peerInputActivitiesDisposable: Disposable? private var recentlyUsedInlineBotsValue: [Peer] = [] private var recentlyUsedInlineBotsDisposable: Disposable? private var unpinMessageDisposable: MetaDisposable? private let typingActivityPromise = Promise(false) private var inputActivityDisposable: Disposable? private var recordingActivityValue: ChatRecordingActivity = .none private let recordingActivityPromise = ValuePromise(.none, ignoreRepeated: true) private var recordingActivityDisposable: Disposable? private var searchDisposable: MetaDisposable? private var historyNavigationStack = ChatHistoryNavigationStack() let canReadHistory = ValuePromise(true, ignoreRepeated: true) private var canReadHistoryValue = false private var canReadHistoryDisposable: Disposable? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings private var automaticMediaDownloadSettingsDisposable: Disposable? private var applicationInForegroundDisposable: Disposable? private var checkedPeerChatServiceActions = false private var raiseToListen: RaiseToListenManager? private var voicePlaylistDidEndTimestamp: Double = 0.0 private weak var silentPostTooltipController: TooltipController? private weak var mediaRecordingModeTooltipController: TooltipController? private var screenCaptureEventsDisposable: Disposable? private let chatAdditionalDataDisposable = MetaDisposable() private var beginMediaRecordingRequestId: Int = 0 var purposefulAction: (() -> Void)? public init(account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false)) { let _ = ChatControllerCount.modify { value in return value + 1 } self.account = account self.chatLocation = chatLocation self.messageId = messageId self.botStart = botStart var locationBroadcastPanelSource: LocationBroadcastPanelSource switch chatLocation { case let .peer(peerId): locationBroadcastPanelSource = .peer(peerId) self.chatLocationInfoData = .peer(Promise()) case .group: locationBroadcastPanelSource = .none self.chatLocationInfoData = .group(Promise()) } self.presentationData = (account.applicationContext as! TelegramApplicationContext).currentPresentationData.with { $0 } self.automaticMediaDownloadSettings = (account.applicationContext as! TelegramApplicationContext).currentAutomaticMediaDownloadSettings.with { $0 } self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, fontSize: self.presentationData.fontSize, accountPeerId: account.peerId, mode: mode, chatLocation: chatLocation) var mediaAccessoryPanelVisibility = MediaAccessoryPanelVisibility.none if case .standard = mode { mediaAccessoryPanelVisibility = .specific(size: .compact) } else { locationBroadcastPanelSource = .none } let navigationBarPresentationData: NavigationBarPresentationData? switch mode { case .inline: navigationBarPresentationData = nil default: navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) } super.init(account: account, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource) 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 } strongSelf.chatDisplayNode.scrollToTop() } self.attemptNavigation = { [weak self] action in guard let strongSelf = self else { return true } if let _ = strongSelf.presentationInterfaceState.inputTextPanelState.mediaRecordingState { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_DiscardVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { self?.stopMediaRecorder() action() })]), in: .window(.root)) return false } return true } let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message, mode in guard let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) else { return false } strongSelf.commitPurposefulAction() var openMessageByAction: Bool = false for media in message.media { if let action = media as? TelegramMediaAction { switch action.action { case .pinnedMessageUpdated: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId)) break } } case let .photoUpdated(image): openMessageByAction = image != nil case .gameScore: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId)) break } } default: break } if !openMessageByAction { return true } } } return openChatMessage(account: account, message: message, standalone: false, reverseMessageGalleryOrder: false, stream: mode == .stream, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: { self?.chatDisplayNode.dismissInput() }, present: { c, a in self?.present(c, in: .window(.root), with: a, blockInteraction: true) }, transitionNode: { messageId, media in var selectedNode: (ASDisplayNode, () -> 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) { 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) }, openPeer: { peer, navigation in self?.openPeer(peerId: peer.id, navigation: navigation, fromMessage: nil) }, callPeer: { peerId in self?.controllerInteraction?.callPeer(peerId) }, enqueueMessage: { message in self?.sendMessages([message]) }, sendSticker: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { fileReference in self?.controllerInteraction?.sendSticker(fileReference, false) } : nil, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in if let strongSelf = self { strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).start(next: { entry in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { var messageIdAndMedia: [MessageId: [Media]] = [:] if let entry = entry, 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).start(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() } } } })) } }) }, openPeer: { [weak self] id, navigation, fromMessage in self?.openPeer(peerId: id, navigation: navigation, fromMessage: fromMessage) }, openPeerMention: { [weak self] name in self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, node, frame in guard let strongSelf = self, strongSelf.isNodeLoaded else { return } if let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) { var updatedMessages = messages for i in 0 ..< updatedMessages.count { if updatedMessages[i].id == message.id { let message = updatedMessages.remove(at: i) updatedMessages.insert(message, at: 0) break } } let _ = contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, account: strongSelf.account, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, interfaceInteraction: strongSelf.interfaceInteraction).start(next: { actions in guard let strongSelf = self, !actions.isEmpty else { return } var contextMenuController: ContextMenuController? var contextActions: [ContextMenuAction] = [] var sheetActions: [ChatMessageContextMenuSheetAction] = [] for action in actions { switch action { case let .context(contextAction): contextActions.append(contextAction) case let .sheet(sheetAction): sheetActions.append(sheetAction) } } var hasActions = false for media in updatedMessages[0].media { if media is TelegramMediaAction || media is TelegramMediaExpiredContent { hasActions = true break } } if !contextActions.isEmpty { contextMenuController = ContextMenuController(actions: contextActions, catchTapsOutside: true, hasHapticFeedback: hasActions) } contextMenuController?.dismissed = { if let strongSelf = self { strongSelf.chatDisplayNode.displayMessageActionSheet(stableId: nil, sheetActions: nil, displayContextMenuController: nil) } } if hasActions { if let contextMenuController = contextMenuController { strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { guard let strongSelf = self else { return nil } return (node, frame, strongSelf.displayNode, strongSelf.displayNode.bounds) })) } } else { strongSelf.chatDisplayNode.displayMessageActionSheet(stableId: updatedMessages[0].stableId, sheetActions: sheetActions, displayContextMenuController: contextMenuController.flatMap { ($0, node, frame) }) } }) } }, navigateToMessage: { [weak self] fromId, id in self?.navigateToMessage(from: fromId, to: .id(id)) }, clickThroughMessage: { [weak self] in self?.chatDisplayNode.dismissInput() }, toggleMessagesSelection: { [weak self] ids, value in guard let strongSelf = self, strongSelf.isNodeLoaded else { return } strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessages(ids, value: value) } }) }, sendMessage: { [weak self] text in guard let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) else { return } strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) var attributes: [MessageAttribute] = [] let entities = generateTextEntities(text, enabledTypes: .all) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } strongSelf.sendMessages([.message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) }, sendSticker: { [weak self] fileReference, clearInput in if let strongSelf = self { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in var current = current current = current.updatedInterfaceState { interfaceState in var interfaceState = interfaceState interfaceState = interfaceState.withUpdatedReplyMessageId(nil) if clearInput { interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) } return interfaceState }.updatedInputMode { current in if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil { return .media(mode: mode, expanded: nil) } return current } return current }) } }) strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, sendGif: { [weak self] fileReference in if let strongSelf = self { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }.updatedInputMode { current in if case let .media(mode, maybeExpanded) = current, let expanded = maybeExpanded, case .content = expanded { return .media(mode: mode, expanded: nil) } return current } }) } }) strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, requestMessageActionCallback: { [weak self] messageId, data, isGame in if let strongSelf = self { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { 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.messageActionCallbackDisposable.set(((requestMessageActionCallback(account: strongSelf.account, messageId: messageId, isGame: isGame, data: data) |> afterDisposed { Queue.mainQueue().async { if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedTitlePanelContext { if let index = $0.index(where: { switch $0 { case .requestInProgress: return true default: return false } }) { var updatedContexts = $0 updatedContexts.remove(at: index) return updatedContexts } return $0 } }) } } }) |> deliverOnMainQueue).start(next: { result in if let strongSelf = self { switch result { case .none: break case let .alert(text): strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), 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 = .single(text) let noMessage: Signal = .single(nil) let delayedNoMessage: Signal = noMessage |> delay(1.0, queue: Queue.mainQueue()) strongSelf.botCallbackAlertMessage.set(message |> then(delayedNoMessage)) case let .url(url): if isGame { strongSelf.chatDisplayNode.dismissInput() (strongSelf.navigationController as? NavigationController)?.pushViewController(GameController(account: strongSelf.account, url: url, message: message)) } else { strongSelf.openUrl(url, concealed: false) } } } })) } } }, activateSwitchInline: { [weak self] peerId, inputString in guard let strongSelf = self else { return } if let botStart = strongSelf.botStart, case let .automatic(returnToPeerId) = botStart.behavior { strongSelf.openPeer(peerId: returnToPeerId, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), messageId: nil), fromMessage: nil) } else { strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), messageId: nil), fromMessage: nil) } }, openUrl: { [weak self] url, concealed, _ in if let strongSelf = self { strongSelf.openUrl(url, concealed: concealed) } }, shareCurrentLocation: { [weak self] in if let strongSelf = self { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), 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.account.telegramApplicationContext.locationManager { let _ = (currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0) |> deliverOnMainQueue).start(next: { coordinate in if let strongSelf = self { if let coordinate = coordinate { strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil)), replyToMessageId: nil, localGroupingKey: nil)]) } else { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), 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 { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), 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.account.postbox.loadedPeerWithId(strongSelf.account.peerId) |> deliverOnMainQueue).start(next: { peer in if let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, localGroupingKey: nil)]) } }) } })]), in: .window(.root)) } }, sendBotCommand: { [weak self] messageId, command in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) 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 .group: postAsReply = true } } strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } }) } }) var attributes: [MessageAttribute] = [] let entities = generateTextEntities(command, enabledTypes: .all) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } strongSelf.sendMessages([.message(text: command, attributes: attributes, mediaReference: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil, localGroupingKey: nil)]) } }, openInstantPage: { [weak self] message in if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.navigationController as? NavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { openChatInstantPage(account: strongSelf.account, message: message, navigationController: navigationController) } }, openHashtag: { [weak self] peerName, hashtag in guard let strongSelf = self else { return } if strongSelf.resolvePeerByNameDisposable == nil { strongSelf.resolvePeerByNameDisposable = MetaDisposable() } var resolveSignal: Signal if let peerName = peerName { resolveSignal = resolvePeerByName(account: strongSelf.account, name: peerName) |> mapToSignal { peerId -> Signal in if let peerId = peerId { return account.postbox.loadedPeerWithId(peerId) |> map(Optional.init) } else { return .single(nil) } } } else if case let .peer(peerId) = strongSelf.chatLocation { resolveSignal = account.postbox.loadedPeerWithId(peerId) |> map(Optional.init) } else { resolveSignal = .single(nil) } var cancelImpl: (() -> Void)? let presentationData = strongSelf.presentationData let progressSignal = Signal { subscriber in let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, 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 = { self?.resolvePeerByNameDisposable?.set(nil) } strongSelf.resolvePeerByNameDisposable?.set((resolveSignal |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self, !hashtag.isEmpty { let searchController = HashtagSearchController(account: strongSelf.account, peer: peer, query: hashtag) (strongSelf.navigationController as? NavigationController)?.pushViewController(searchController) } })) }, 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 if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id) { let shareController = ShareController(account: strongSelf.account, subject: .messages(messages)) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(shareController, in: .window(.root)) } }, presentController: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) }, navigationController: { [weak self] in return self?.navigationController as? NavigationController }, presentGlobalOverlayController: { [weak self] controller, arguments in self?.presentInGlobalOverlay(controller, with: arguments) }, callPeer: { [weak self] peerId in if let strongSelf = self { strongSelf.commitPurposefulAction() let _ = (account.viewTracker.peerView(peerId) |> take(1) |> map { view -> Peer? in return peerViewMainPeer(view) } |> deliverOnMainQueue).start(next: { peer in guard let peer = peer else { return } if let cachedUserData = strongSelf.peerView?.cachedData as? CachedUserData, cachedUserData.callsPrivate { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_ConnectionErrorTitle, text: presentationData.strings.Call_PrivacyErrorMessage(peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return } let callResult = account.telegramApplicationContext.callManager?.requestCall(peerId: peer.id, endCurrentIfAny: false) if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { if currentPeerId == peer.id { account.telegramApplicationContext.navigateToCurrentCall?() } else { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let _ = (account.postbox.transaction { transaction -> (Peer?, Peer?) in return (transaction.getPeer(peer.id), transaction.getPeer(currentPeerId)) } |> deliverOnMainQueue).start(next: { peer, current in if let peer = peer, let current = current { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peer.id, endCurrentIfAny: true) })]), in: .window(.root)) } }) } } }) } }, longTap: { [weak self] action in if let strongSelf = self { switch action { case let .url(url): var cleanUrl = url var canAddToReadingList = true let canOpenIn = availableOpenInOptions(applicationContext: strongSelf.account.telegramApplicationContext, item: .url(url: url)).count > 1 let mailtoString = "mailto:" let telString = "tel:" var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen var phoneNumber: String? if cleanUrl.hasPrefix(mailtoString) { canAddToReadingList = false cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]) } else if cleanUrl.hasPrefix(telString) { canAddToReadingList = false phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...]) cleanUrl = phoneNumber! openText = strongSelf.presentationData.strings.UserInfo_PhoneCall } else if canOpenIn { openText = strongSelf.presentationData.strings.Conversation_FileOpenIn } let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: cleanUrl)) items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { if canOpenIn { strongSelf.openUrlIn(url) } else { strongSelf.openUrl(url, concealed: false) } } })) if let phoneNumber = phoneNumber { items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.controllerInteraction?.addContact(phoneNumber) } })) } items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() UIPasteboard.general.string = cleanUrl })) if canAddToReadingList { items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let link = URL(string: url) { let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) } })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .peerMention(peerId, mention): let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) var items: [ActionSheetItem] = [] if !mention.isEmpty { items.append(ActionSheetTextItem(title: mention)) } items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil, messageId: nil), fromMessage: nil) } })) if !mention.isEmpty { items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() UIPasteboard.general.string = mention })) } actionSheet.setItemGroups([ActionSheetItemGroup(items:items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .mention(mention): let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: mention), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.openPeerMention(mention) } }), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() UIPasteboard.general.string = mention }) ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .command(command): let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: command)) if canSendMessagesToChat(strongSelf.presentationInterfaceState) { items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.sendMessages([.message(text: command, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]) } })) } items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() UIPasteboard.general.string = command })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .hashtag(hashtag): let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: hashtag), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { let peerSignal: Signal if case let .peer(peerId) = strongSelf.chatLocation { peerSignal = strongSelf.account.postbox.loadedPeerWithId(peerId) |> map(Optional.init) } else { peerSignal = .single(nil) } let _ = (peerSignal |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self { let searchController = HashtagSearchController(account: strongSelf.account, peer: peer, query: hashtag) (strongSelf.navigationController as? NavigationController)?.pushViewController(searchController) } }) } }), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() UIPasteboard.general.string = hashtag }) ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) } } }, openCheckoutOrReceipt: { [weak self] messageId in if let strongSelf = self { strongSelf.commitPurposefulAction() if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { for media in message.media { if let invoice = media as? TelegramMediaInvoice { strongSelf.chatDisplayNode.dismissInput() if let receiptMessageId = invoice.receiptMessageId { strongSelf.present(BotReceiptController(account: strongSelf.account, invoice: invoice, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { strongSelf.present(BotCheckoutController(account: strongSelf.account, invoice: invoice, messageId: messageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } } } } }, openSearch: { }, setupReply: { [weak self] messageId in self?.interfaceInteraction?.setupReplyMessage(messageId) }, canSetupReply: { [weak self] message in if !message.flags.contains(.Incoming) { if !message.flags.intersection([.Failed, .Sending, .Unsent]).isEmpty { return false } } if let strongSelf = self { return canReplyInChat(strongSelf.presentationInterfaceState) } return false }, navigateToFirstDateMessage: { [weak self] timestamp in guard let strongSelf = self else { return } switch strongSelf.chatLocation { case let .peer(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, animated: true, completion: nil) default: break } }, requestRedeliveryOfFailedMessages: { [weak self] id in guard let strongSelf = self else { return } let _ = (strongSelf.account.postbox.transaction { transaction -> [Message] in return transaction.getMessageFailedGroup(id) ?? [] } |> deliverOnMainQueue).start(next: { messages in guard let strongSelf = self, !messages.isEmpty else { return } let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_MessageDialogRetry, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() guard let strongSelf = self else { return } let _ = resendMessages(account: strongSelf.account, messageIds: [id]).start() })) if messages.count != 1 { items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_MessageDialogRetryAll(messages.count).0, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() guard let strongSelf = self else { return } let _ = resendMessages(account: strongSelf.account, messageIds: messages.map({ $0.id })).start() })) } items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_MessageDialogDelete, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() guard let strongSelf = self else { return } let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: messages.map({ $0.id }), type: .forLocalPeer).start() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) }) }, addContact: { [weak self] phoneNumber in if let strongSelf = self { openAddContact(account: strongSelf.account, phoneNumber: phoneNumber, present: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) }) } }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) } }, cancelInteractiveKeyboardGestures: { [weak self] in (self?.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() self?.chatDisplayNode.cancelInteractiveKeyboardGestures() }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings) self.controllerInteraction = controllerInteraction self.chatTitleView = ChatTitleView(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) self.navigationItem.titleView = self.chatTitleView self.chatTitleView?.pressed = { [weak self] in if let strongSelf = self { if strongSelf.chatLocation == .peer(strongSelf.account.peerId) { (strongSelf.navigationController as? NavigationController)?.pushViewController(PeerMediaCollectionController(account: strongSelf.account, peerId: strongSelf.account.peerId)) } else { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedTitlePanelContext { if let index = $0.index(where: { switch $0 { case .chatInfo: return true default: return false } }) { var updatedContexts = $0 updatedContexts.remove(at: index) return updatedContexts } else { var updatedContexts = $0 updatedContexts.append(.chatInfo) return updatedContexts.sorted() } } }) } } } let chatInfoButtonItem: UIBarButtonItem switch chatLocation { case .peer: let avatarNode = ChatAvatarNavigationNode() avatarNode.chatController = self chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! case .group: chatInfoButtonItem = UIBarButtonItem(customDisplayNode: ChatMultipleAvatarsNavigationNode())! } chatInfoButtonItem.target = self chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction) self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: chatInfoButtonItem) self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in if let botStart = botStart, case .interactive = botStart.behavior { return state.updatedBotStartPayload(botStart.payload) } else { return state } }) switch chatLocation { case let .peer(peerId): if case let .peer(peerView) = self.chatLocationInfoData { peerView.set(account.viewTracker.peerView(peerId)) var onlineMemberCount: Signal = .single(nil) if peerId.namespace == Namespaces.Peer.CloudChannel { onlineMemberCount = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recentOnline(postbox: account.postbox, network: account.network, accountPeerId: account.peerId, peerId: peerId) |> map(Optional.init) } self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount) |> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount in if let strongSelf = self { if let peer = peerViewMainPeer(peerView) { strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, onlineMemberCount: onlineMemberCount) (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) } if strongSelf.peerView === peerView { return } 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 } } strongSelf.peerView = peerView if wasGroupChannel != isGroupChannel { if let isGroupChannel = isGroupChannel, isGroupChannel { let (recentDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: strongSelf.account.postbox, network: strongSelf.account.network, accountPeerId: account.peerId, peerId: peerView.peerId, updated: { _ in }) let (adminsDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.account.postbox, network: strongSelf.account.network, accountPeerId: 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.peerView = peerView } var peerIsMuted = false if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { peerIsMuted = true } } var renderedPeer: RenderedPeer? var isContact: Bool = false if let peer = peerView.peers[peerView.peerId] { isContact = peerView.peerIsContact var peers = SimpleDictionary() 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) } var isNotAccessible: Bool = false if let cachedChannelData = peerView.cachedData as? CachedChannelData { isNotAccessible = cachedChannelData.isNotAccessible } var explicitelyCanPinMessages: Bool = false if let cachedUserData = peerView.cachedData as? CachedUserData { explicitelyCanPinMessages = cachedUserData.canPinMessages } else if peerView.peerId == 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 } strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { return $0.updatedPeer { _ in return renderedPeer }.updatedIsNotAccessible(isNotAccessible).updatedIsContact(isContact).updatedPeerIsMuted(peerIsMuted).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages) }) if !strongSelf.didSetChatLocationInfoReady { strongSelf.didSetChatLocationInfoReady = true strongSelf._chatLocationInfoReady.set(.single(true)) } } })) } case let .group(groupId): if case let .group(topPeersView) = self.chatLocationInfoData { let key: PostboxViewKey = .chatListTopPeers(groupId: groupId) topPeersView.set(account.postbox.combinedView(keys: [key]) |> mapToSignal { view -> Signal in if let entry = view.views[key] as? ChatListTopPeersView { return .single(entry) } return .complete() }) self.peerDisposable.set((topPeersView.get() |> deliverOnMainQueue).start(next: { [weak self] topPeersView in if let strongSelf = self { strongSelf.chatTitleView?.titleContent = .group(topPeersView.peers) (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatMultipleAvatarsNavigationNode)?.setPeers(account: strongSelf.account, peers: topPeersView.peers, animated: strongSelf.didSetChatLocationInfoReady) if !strongSelf.didSetChatLocationInfoReady { strongSelf.didSetChatLocationInfoReady = true strongSelf._chatLocationInfoReady.set(.single(true)) } } })) } } self.botCallbackAlertMessageDisposable = (self.botCallbackAlertMessage.get() |> deliverOnMainQueue).start(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.index(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.index(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).start(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 if let audioRecorder = audioRecorder { if panelState.mediaRecordingState == nil { return panelState.withUpdatedMediaRecordingState(.audio(recorder: audioRecorder, isLocked: false)) } } else { return panelState.withUpdatedMediaRecordingState(nil) } return panelState } }) if let audioRecorder = audioRecorder { if !audioRecorder.beginWithTone { strongSelf.recorderFeedback?.impact(.light) } audioRecorder.start() } } } }) self.videoRecorderDisposable = (self.videoRecorder.get() |> deliverOnMainQueue).start(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 { return panelState.withUpdatedMediaRecordingState(.video(status: .recording(videoRecorder.audioStatus), isLocked: false)) } } else { return panelState.withUpdatedMediaRecordingState(nil) } return panelState } }) if let videoRecorder = videoRecorder { strongSelf.recorderFeedback?.impact(.light) videoRecorder.onDismiss = { if let strongSelf = self { strongSelf.videoRecorder.set(.single(nil)) } } videoRecorder.onStop = { if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInputTextPanelState { panelState in return panelState.withUpdatedMediaRecordingState(.video(status: .editing, isLocked: false)) } }) } } strongSelf.present(videoRecorder, in: .window(.root)) } if let previousVideoRecorderValue = previousVideoRecorderValue { previousVideoRecorderValue.dismissVideo() } } } }) if let botStart = botStart, case .automatic = botStart.behavior { self.startBot(botStart.payload) } self.inputActivityDisposable = (self.typingActivityPromise.get() |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .typingText, isPresent: value) } }) self.recordingActivityDisposable = (self.recordingActivityPromise.get() |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { switch value { case .voice: strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingVoice, isPresent: true) strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingInstantVideo, isPresent: false) case .instantVideo: strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingVoice, isPresent: false) strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingInstantVideo, isPresent: true) case .none: strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingVoice, isPresent: false) strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .recordingInstantVideo, isPresent: false) } } }) self.presentationDataDisposable = (account.telegramApplicationContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme let previousStrings = strongSelf.presentationData.strings let previousChatWallpaper = strongSelf.presentationData.chatWallpaper strongSelf.presentationData = presentationData if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings || presentationData.chatWallpaper != previousChatWallpaper { strongSelf.themeAndStringsUpdated() } } }) self.automaticMediaDownloadSettingsDisposable = (account.telegramApplicationContext.automaticMediaDownloadSettings |> deliverOnMainQueue).start(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() } } }) self.applicationInForegroundDisposable = (account.telegramApplicationContext.applicationBindings.applicationInForeground |> distinctUntilChanged |> deliverOn(Queue.mainQueue())).start(next: { [weak self] value in if let strongSelf = self, strongSelf.isNodeLoaded { if !value { strongSelf.saveInterfaceState() strongSelf.raiseToListen?.applicationResignedActive() strongSelf.stopMediaRecorder() } } }) self.canReadHistoryDisposable = (combineLatest((self.account.applicationContext as! TelegramApplicationContext).applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in return a && b } |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self, strongSelf.canReadHistoryValue != value { strongSelf.canReadHistoryValue = value strongSelf.raiseToListen?.enabled = value } }) self.networkStateDisposable = (account.networkState |> deliverOnMainQueue).start(next: { [weak self] state in if let strongSelf = self { strongSelf.chatTitleView?.networkState = state } }) if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { self.screenCaptureEventsDisposable = screenCaptureEvents().start(next: { [weak self] _ in if let strongSelf = self, strongSelf.canReadHistoryValue, strongSelf.traceVisibility() { let _ = addSecretChatMessageScreenshot(account: account, peerId: peerId).start() } }) } } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { let _ = ChatControllerCount.modify { value in return value - 1 } self.historyStateDisposable?.dispose() self.messageIndexDisposable.dispose() self.navigationActionDisposable.dispose() self.galleryHiddenMesageAndMediaDisposable.dispose() self.temporaryHiddenGalleryMediaDisposable.dispose() self.peerDisposable.dispose() self.messageContextDisposable.dispose() self.controllerNavigationDisposable.dispose() self.sentMessageEventsDisposable.dispose() self.failedMessageEventsDisposable.dispose() self.messageActionCallbackDisposable.dispose() self.editMessageDisposable.dispose() self.enqueueMediaMessageDisposable.dispose() self.resolvePeerByNameDisposable?.dispose() self.shareStatusDisposable?.dispose() self.botCallbackAlertMessageDisposable?.dispose() for (_, info) in self.contextQueryStates { info.1.dispose() } self.urlPreviewQueryState?.1.dispose() self.audioRecorderDisposable?.dispose() self.videoRecorderDisposable?.dispose() self.buttonKeyboardMessageDisposable?.dispose() self.cachedDataDisposable?.dispose() self.resolveUrlDisposable?.dispose() self.chatUnreadCountDisposable?.dispose() self.chatUnreadMentionCountDisposable?.dispose() self.peerInputActivitiesDisposable?.dispose() self.recentlyUsedInlineBotsDisposable?.dispose() self.unpinMessageDisposable?.dispose() self.inputActivityDisposable?.dispose() self.recordingActivityDisposable?.dispose() self.presentationDataDisposable?.dispose() self.searchDisposable?.dispose() self.applicationInForegroundDisposable?.dispose() self.canReadHistoryDisposable?.dispose() self.networkStateDisposable?.dispose() self.screenCaptureEventsDisposable?.dispose() self.chatAdditionalDataDisposable.dispose() self.shareStatusDisposable?.dispose() } public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { self.updateChatPresentationInterfaceState(animated: false, interactive: false, { return $0.updatedMode(mode) }) } var chatDisplayNode: ChatControllerNode { get { return super.displayNode as! ChatControllerNode } } private func themeAndStringsUpdated() { self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) self.chatTitleView?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in var state = state state = state.updatedTheme(self.presentationData.theme) state = state.updatedStrings(self.presentationData.strings) state = state.updatedDateTimeFormat(self.presentationData.dateTimeFormat) state = state.updatedChatWallpaper(self.presentationData.chatWallpaper) return state }) } override public func loadDisplayNode() { self.displayNode = ChatControllerNode(account: self.account, chatLocation: self.chatLocation, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, controller: self) self.chatDisplayNode.peerView = self.peerView let initialData = self.chatDisplayNode.historyNode.initialData |> take(1) |> beforeNext { [weak self] combinedInitialData in guard let strongSelf = self, let combinedInitialData = combinedInitialData else { return } if let interfaceState = combinedInitialData.initialData?.chatInterfaceState as? ChatInterfaceState { var pinnedMessageId: MessageId? var peerIsBlocked: Bool = false var canReport: Bool = false var callsAvailable: Bool = true var callsPrivate: Bool = false if let cachedData = combinedInitialData.cachedData as? CachedChannelData { pinnedMessageId = cachedData.pinnedMessageId canReport = cachedData.reportStatus == .canReport } else if let cachedData = combinedInitialData.cachedData as? CachedUserData { peerIsBlocked = cachedData.isBlocked canReport = cachedData.reportStatus == .canReport callsAvailable = cachedData.callsAvailable callsPrivate = cachedData.callsPrivate pinnedMessageId = cachedData.pinnedMessageId } else if let cachedData = combinedInitialData.cachedData as? CachedGroupData { canReport = cachedData.reportStatus == .canReport pinnedMessageId = cachedData.pinnedMessageId } else if let cachedData = combinedInitialData.cachedData as? CachedSecretChatData { canReport = cachedData.reportStatus == .canReport } var pinnedMessage: Message? if let pinnedMessageId = pinnedMessageId { if let cachedDataMessages = combinedInitialData.cachedDataMessages { pinnedMessage = cachedDataMessages[pinnedMessageId] } } strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { updated in var updated = updated updated = updated.updatedInterfaceState({ _ in return interfaceState }) updated = updated.updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage) updated = updated.updatedPinnedMessageId(pinnedMessageId) updated = updated.updatedPinnedMessage(pinnedMessage) updated = updated.updatedPeerIsBlocked(peerIsBlocked) updated = updated.updatedCanReportPeer(canReport) updated = updated.updatedCallsAvailable(callsAvailable) updated = updated.updatedCallsPrivate(callsPrivate) updated = updated.updatedTitlePanelContext({ context in if pinnedMessageId != nil { if !context.contains(where: { switch $0 { case .pinnedMessage: return true default: return false } }) { var updatedContexts = context updatedContexts.append(.pinnedMessage) return updatedContexts.sorted() } else { return context } } else { if let index = context.index(where: { switch $0 { case .pinnedMessage: return true default: return false } }) { var updatedContexts = context updatedContexts.remove(at: index) return updatedContexts } else { return context } } }) if let editMessage = interfaceState.editMessage, let message = combinedInitialData.initialData?.associatedMessages[editMessage.messageId] { updated = updatedChatEditInterfaceMessagetState(state: updated, message: message) } return updated }) } if let readStateData = combinedInitialData.readStateData { if case let .peer(peerId) = strongSelf.chatLocation, let peerReadStateData = readStateData[peerId], let notificationSettings = peerReadStateData.notificationSettings { let inAppSettings = peerReadStateData.inAppNotificationSettings ?? InAppNotificationSettings.defaultSettings let (count, _) = renderedTotalUnreadCount(inAppSettings: inAppSettings, totalUnreadState: peerReadStateData.totalState ?? ChatListTotalUnreadState(absoluteCounters: [:], filteredCounters: [:])) var globalRemainingUnreadChatCount = count if !notificationSettings.isRemovedFromTotalUnreadCount && peerReadStateData.unreadCount > 0 { if case .messages = inAppSettings.totalUnreadCountDisplayCategory { globalRemainingUnreadChatCount -= peerReadStateData.unreadCount } else { globalRemainingUnreadChatCount -= 1 } } if globalRemainingUnreadChatCount > 0 { strongSelf.navigationItem.badge = "\(globalRemainingUnreadChatCount)" } else { strongSelf.navigationItem.badge = "" } } } } self.buttonKeyboardMessageDisposable = self.chatDisplayNode.historyNode.buttonKeyboardMessage.start(next: { [weak self] message in if let strongSelf = self { var buttonKeyboardMessageUpdated = false if let currentButtonKeyboardMessage = strongSelf.presentationInterfaceState.keyboardButtonsMessage, let message = message { if currentButtonKeyboardMessage.id != message.id || currentButtonKeyboardMessage.stableVersion != message.stableVersion { buttonKeyboardMessageUpdated = true } } else if (strongSelf.presentationInterfaceState.keyboardButtonsMessage != nil) != (message != nil) { buttonKeyboardMessageUpdated = true } if buttonKeyboardMessageUpdated { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedKeyboardButtonsMessage(message) }) } } }) self.cachedDataDisposable = self.chatDisplayNode.historyNode.cachedPeerDataAndMessages.start(next: { [weak self] cachedData, messages in if let strongSelf = self { var pinnedMessageId: MessageId? var peerIsBlocked: Bool = false var canReport: Bool = false var callsAvailable: Bool = false var callsPrivate: Bool = false if let cachedData = cachedData as? CachedChannelData { pinnedMessageId = cachedData.pinnedMessageId canReport = cachedData.reportStatus == .canReport } else if let cachedData = cachedData as? CachedUserData { peerIsBlocked = cachedData.isBlocked canReport = cachedData.reportStatus == .canReport callsAvailable = cachedData.callsAvailable callsPrivate = cachedData.callsPrivate pinnedMessageId = cachedData.pinnedMessageId } else if let cachedData = cachedData as? CachedGroupData { canReport = cachedData.reportStatus == .canReport pinnedMessageId = cachedData.pinnedMessageId } else if let cachedData = cachedData as? CachedSecretChatData { canReport = cachedData.reportStatus == .canReport } var pinnedMessage: Message? if let pinnedMessageId = pinnedMessageId { pinnedMessage = messages?[pinnedMessageId] } var pinnedMessageUpdated = false if let current = strongSelf.presentationInterfaceState.pinnedMessage, let updated = pinnedMessage { if current.id != updated.id || current.stableVersion != updated.stableVersion { pinnedMessageUpdated = true } } else if (strongSelf.presentationInterfaceState.pinnedMessage != nil) != (pinnedMessage != nil) { pinnedMessageUpdated = true } if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || strongSelf.presentationInterfaceState.canReportPeer != canReport || pinnedMessageUpdated { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in return state.updatedPinnedMessageId(pinnedMessageId).updatedPinnedMessage(pinnedMessage).updatedPeerIsBlocked(peerIsBlocked).updatedCanReportPeer(canReport).updatedCallsAvailable(callsAvailable).updatedCallsPrivate(callsPrivate).updatedTitlePanelContext({ context in if pinnedMessageId != nil { if !context.contains(where: { switch $0 { case .pinnedMessage: return true default: return false } }) { var updatedContexts = context updatedContexts.append(.pinnedMessage) return updatedContexts.sorted() } else { return context } } else { if let index = context.index(where: { switch $0 { case .pinnedMessage: return true default: return false } }) { var updatedContexts = context updatedContexts.remove(at: index) return updatedContexts } else { return context } } }) }) } } }) self.historyStateDisposable = self.chatDisplayNode.historyNode.historyState.get().start(next: { [weak self] state in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: strongSelf.isViewLoaded && strongSelf.view.window != nil, { $0.updatedChatHistoryState(state) }) } }) self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyState.get(), self._chatLocationInfoReady.get(), initialData) |> map { _, chatLocationInfoReady, _ in return chatLocationInfoReady }) self.chatDisplayNode.historyNode.contentPositionChanged = { [weak self] offset in if let strongSelf = self { let offsetAlpha: CGFloat switch offset { case let .known(offset): if offset < 40.0 { offsetAlpha = 0.0 } else { offsetAlpha = 1.0 } case .unknown: offsetAlpha = 1.0 case .none: offsetAlpha = 0.0 } strongSelf.chatDisplayNode.navigateButtons.displayDownButton = !offsetAlpha.isZero } } self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toIndex in if let strongSelf = self, case let .message(index) = toIndex { if let controllerInteraction = strongSelf.controllerInteraction { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) { let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) controllerInteraction.highlightedState = highlightedState strongSelf.updateItemNodesHighlightedStates(animated: true) strongSelf.messageContextDisposable.set((Signal.complete() |> delay(0.7, queue: Queue.mainQueue())).start(completed: { if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { if controllerInteraction.highlightedState == highlightedState { controllerInteraction.highlightedState = nil strongSelf.updateItemNodesHighlightedStates(animated: true) } } })) } } } } self.chatDisplayNode.historyNode.maxVisibleMessageIndexUpdated = { [weak self] index in if let strongSelf = self, !strongSelf.historyNavigationStack.isEmpty { strongSelf.historyNavigationStack.filterOutIndicesLessThan(index) } } self.chatDisplayNode.requestLayout = { [weak self] transition in self?.requestLayout(transition: transition) } self.chatDisplayNode.setupSendActionOnViewUpdate = { [weak self] f in self?.chatDisplayNode.historyNode.layoutActionOnViewTransition = { [weak self] transition in f() if let strongSelf = self, let validLayout = strongSelf.validLayout { var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)? strongSelf.chatDisplayNode.containerLayoutUpdated(validLayout, navigationBarHeight: strongSelf.navigationHeight, transition: .animated(duration: 0.2, curve: .easeInOut), listViewTransaction: { updateSizeAndInsets, _, _ in var options = transition.options let _ = options.insert(.Synchronous) let _ = options.insert(.LowLatency) options.remove(.AnimateInsertion) options.insert(.RequestItemInsertionAnimations) let deleteItems = transition.deleteItems.map({ item in return ListViewDeleteItem(index: item.index, directionHint: nil) }) var maxInsertedItem: Int? var insertItems: [ListViewInsertItem] = [] for i in 0 ..< transition.insertItems.count { let item = transition.insertItems[i] if item.directionHint == .Down && (maxInsertedItem == nil || maxInsertedItem! < item.index) { maxInsertedItem = item.index } insertItems.append(ListViewInsertItem(index: item.index, previousIndex: item.previousIndex, item: item.item, directionHint: item.directionHint == .Down ? .Up : nil)) } let scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: 0.2), directionHint: .Up) var stationaryItemRange: (Int, Int)? if let maxInsertedItem = maxInsertedItem { stationaryItemRange = (maxInsertedItem + 1, Int.max) } mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, animateIn: false), updateSizeAndInsets) }) if let mappedTransition = mappedTransition { return mappedTransition } } return (transition, nil) } } self.chatDisplayNode.sendMessages = { [weak self] messages in if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { strongSelf.commitPurposefulAction() let _ = (enqueueMessages(account: strongSelf.account, peerId: peerId, messages: strongSelf.transformEnqueueMessages(messages)) |> deliverOnMainQueue).start(next: { _ in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() } }) } } self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] animated, f in self?.updateChatPresentationInterfaceState(animated: animated, interactive: true, { $0.updatedInterfaceState(f) }) } self.chatDisplayNode.requestUpdateInterfaceState = { [weak self] transition, interactive, f in self?.updateChatPresentationInterfaceState(transition: transition, interactive: interactive, f) } self.chatDisplayNode.displayAttachmentMenu = { [weak self] in guard let strongSelf = self else { return } if case .peer = strongSelf.chatLocation, let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId { let _ = (strongSelf.account.postbox.transaction { transaction -> Message? in return transaction.getMessage(messageId) } |> deliverOnMainQueue).start(next: { message in guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState, case let .media(options) = editMessageState.content else { return } strongSelf.presentAttachmentMenu(editMediaOptions: options) }) } else { strongSelf.presentAttachmentMenu(editMediaOptions: nil) } } self.chatDisplayNode.paste = { [weak self] data in switch data { case let .images(images): self?.displayPasteMenu(images) case let .gif(data): self?.enqueueGifData(data) case let .sticker(image): self?.enqueueStickerImage(image) } } self.chatDisplayNode.updateTypingActivity = { [weak self] value in if let strongSelf = self, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil { if value { strongSelf.typingActivityPromise.set(Signal.single(true) |> then( Signal.single(false) |> delay(4.0, queue: Queue.mainQueue()) )) } else { strongSelf.typingActivityPromise.set(.single(false)) } } } self.chatDisplayNode.dismissUrlPreview = { [weak self] in if let strongSelf = self { if let _ = strongSelf.presentationInterfaceState.interfaceState.editMessage { if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedEditMessage($0.editMessage.flatMap { $0.withUpdatedDisableUrlPreview(link) }) } }) } } else { if let (link, _) = strongSelf.presentationInterfaceState.urlPreview { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedComposeDisableUrlPreview(link) } }) } } } } self.chatDisplayNode.navigateButtons.downPressed = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded { if let messageId = strongSelf.historyNavigationStack.removeLast() { strongSelf.navigateToMessage(from: nil, to: .id(messageId.id), rememberInStack: false) } else { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() } } } self.chatDisplayNode.navigateButtons.mentionsPressed = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded, case let .peer(peerId) = strongSelf.chatLocation { let signal = earliestUnseenPersonalMentionMessage(postbox: strongSelf.account.postbox, network: strongSelf.account.network, accountPeerId: strongSelf.account.peerId, peerId: peerId) strongSelf.navigationActionDisposable.set((signal |> deliverOnMainQueue).start(next: { result in if let strongSelf = self { switch result { case let .result(messageId): if let messageId = messageId { strongSelf.navigateToMessage(from: nil, to: .id(messageId)) } case .loading: break } } })) } } self.chatDisplayNode.navigateButtons.mentionsMenu = { [weak self] in guard let strongSelf = self else { return } let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.WebSearch_RecentSectionClear, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else { return } let _ = clearPeerUnseenPersonalMessagesInteractively(account: strongSelf.account, peerId: peerId).start() }) ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) } let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId in if let strongSelf = self, strongSelf.isNodeLoaded, canSendMessagesToChat(strongSelf.presentationInterfaceState) { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageId(message.id) }).updatedSearch(nil) }) strongSelf.chatDisplayNode.ensureInputViewFocused() } } }, setupEditMessage: { [weak self] messageId in if let strongSelf = self, strongSelf.isNodeLoaded { guard let messageId = messageId else { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in var state = state state = state.updatedInterfaceState { $0.withUpdatedEditMessage(nil) } state = state.updatedEditMessageState(nil) return state }) strongSelf.editMessageDisposable.set(nil) return } if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in var updated = state.updatedInterfaceState { var entities: [MessageTextEntity] = [] for attribute in message.attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { entities = attribute.entities break } } return $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(message.text, entities: entities)), disableUrlPreview: nil)) } updated = updatedChatEditInterfaceMessagetState(state: updated, message: message) updated = updated.updatedInputMode({ _ in return .text }) return updated }) } } }, beginMessageSelection: { [weak self] messageIds in if let strongSelf = self, strongSelf.isNodeLoaded { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true,{ $0.updatedInterfaceState { $0.withUpdatedSelectedMessages(messageIds) } }) } }, deleteSelectedMessages: { [weak self] in if let strongSelf = self { if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { strongSelf.messageContextDisposable.set((chatAvailableMessageActions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds) |> deliverOnMainQueue).start(next: { actions in if let strongSelf = self, !actions.options.isEmpty { if let banAuthor = actions.banAuthor { strongSelf.presentBanMessageOptions(author: banAuthor, messageIds: messageIds, options: actions.options) } else { strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options) } } })) } } }, reportSelectedMessages: { [weak self] in if let strongSelf = self, let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { strongSelf.present(peerReportOptionsController(account: strongSelf.account, subject: .messages(Array(messageIds).sorted()), present: { c, a in self?.present(c, in: .window(.root), with: a) }), in: .window(.root)) } }, reportMessages: { [weak self] messages in if let strongSelf = self, !messages.isEmpty { strongSelf.present(peerReportOptionsController(account: strongSelf.account, subject: .messages(messages.map({ $0.id }).sorted()), present: { c, a in self?.present(c, in: .window(.root), with: a) }), in: .window(.root)) } }, deleteMessages: { [weak self] messages in if let strongSelf = self, !messages.isEmpty { let messageIds = Set(messages.map { $0.id }) strongSelf.messageContextDisposable.set((chatAvailableMessageActions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds) |> deliverOnMainQueue).start(next: { actions in if let strongSelf = self, !actions.options.isEmpty { if let banAuthor = actions.banAuthor { strongSelf.presentBanMessageOptions(author: banAuthor, messageIds: messageIds, options: actions.options) } else { var isAction = false if messages.count == 1 { for media in messages[0].media { if media is TelegramMediaAction { isAction = true } } } if isAction && (actions.options == .deleteGlobally || actions.options == .deleteLocally) { let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: actions.options == .deleteLocally ? .forLocalPeer : .forEveryone).start() } else { strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options) } } } })) } }, forwardSelectedMessages: { [weak self] in if let strongSelf = self { strongSelf.commitPurposefulAction() if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { let forwardMessageIds = Array(forwardMessageIdsSet).sorted() strongSelf.forwardMessages(messageIds: forwardMessageIds) } } }, forwardMessages: { [weak self] messages in if let strongSelf = self, !messages.isEmpty { strongSelf.commitPurposefulAction() let forwardMessageIds = messages.map { $0.id }.sorted() strongSelf.forwardMessages(messageIds: forwardMessageIds) } }, shareSelectedMessages: { [weak self] in if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { strongSelf.commitPurposefulAction() let _ = (strongSelf.account.postbox.transaction { transaction -> [Message] in var messages: [Message] = [] for id in selectedIds { if let message = transaction.getMessage(id) { messages.append(message) } } return messages } |> deliverOnMainQueue).start(next: { messages in if let strongSelf = self, !messages.isEmpty { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) let shareController = ShareController(account: strongSelf.account, subject: .messages(messages.sorted(by: { lhs, rhs in return MessageIndex(lhs) < MessageIndex(rhs) })), externalShare: true, immediateExternalShare: true) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(shareController, in: .window(.root)) } }) } }, updateTextInputStateAndMode: { [weak self] f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in let (updatedState, updatedMode) = f(state.interfaceState.effectiveInputState, state.inputMode) return state.updatedInterfaceState { interfaceState in return interfaceState.withUpdatedEffectiveInputState(updatedState) }.updatedInputMode({ _ in updatedMode }) }) } }, updateInputModeAndDismissedButtonKeyboardMessageId: { [weak self] f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0) return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedButtonKeyboardMessageId(updatedClosedButtonKeyboardMessageId) }) }) }) } }, editMessage: { [weak self] in if let strongSelf = self, let editMessage = strongSelf.presentationInterfaceState.interfaceState.editMessage { var disableUrlPreview = false if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview { if editMessage.disableUrlPreview == link { disableUrlPreview = true } } let editingMessage = strongSelf.editingMessage let text = trimChatInputText(editMessage.inputState.inputText) let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) var entitiesAttribute: TextEntitiesMessageAttribute? if !entities.isEmpty { entitiesAttribute = TextEntitiesMessageAttribute(entities: entities) } let media: RequestEditMessageMedia if let editMediaReference = strongSelf.presentationInterfaceState.editMessageState?.mediaReference { media = .update(editMediaReference) } else { media = .keep } strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.account, messageId: editMessage.messageId, text: text.string, media: media , entities: entitiesAttribute, disableUrlPreview: disableUrlPreview) |> deliverOnMainQueue |> afterDisposed({ editingMessage.set(nil) })).start(next: { result in guard let strongSelf = self else { return } switch result { case let .progress(value): editingMessage.set(value) case .done: editingMessage.set(nil) strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in var state = state state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) state = state.updatedEditMessageState(nil) return state }) } }, error: { error in guard let strongSelf = self else { return } editingMessage.set(nil) let text: String switch error { case .generic: text = strongSelf.presentationData.strings.Channel_EditMessageErrorGeneric case .restricted: text = strongSelf.presentationData.strings.Group_ErrorSendRestrictedMedia } strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { })]), in: .window(.root)) })) } }, beginMessageSearch: { [weak self] domain, query in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in return current.updatedTitlePanelContext { if let index = $0.index(where: { switch $0 { case .chatInfo: return true default: return false } }) { var updatedContexts = $0 updatedContexts.remove(at: index) return updatedContexts } else { return $0 } }.updatedSearch(current.search == nil ? ChatSearchData(domain: domain).withUpdatedQuery(query) : current.search?.withUpdatedDomain(domain).withUpdatedQuery(query)) }) } }, dismissMessageSearch: { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in return current.updatedSearch(nil) }) } }, updateMessageSearch: { [weak self] query in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search { return current.updatedSearch(data.withUpdatedQuery(query)) } else { return current } }) } }, navigateMessageSearch: { [weak self] action in if let strongSelf = self { var navigateIndex: MessageIndex? strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search, let resultsState = data.resultsState { if let currentId = resultsState.currentId, let index = resultsState.messageIndices.index(where: { $0.id == currentId }) { var updatedIndex: Int? switch action { case .earlier: if index != 0 { updatedIndex = index - 1 } case .later: if index != resultsState.messageIndices.count - 1 { updatedIndex = index + 1 } } if let updatedIndex = updatedIndex { navigateIndex = resultsState.messageIndices[updatedIndex] return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: resultsState.messageIndices, currentId: resultsState.messageIndices[updatedIndex].id, totalCount: resultsState.totalCount, complete: resultsState.complete))) } } } return current }) if let navigateIndex = navigateIndex { switch strongSelf.chatLocation { case .peer: strongSelf.navigateToMessage(from: nil, to: .id(navigateIndex.id)) case .group: strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex)) } } } }, openCalendarSearch: { [weak self] in if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { strongSelf.chatDisplayNode.dismissInput() let controller = ChatDateSelectionSheet(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, completion: { timestamp in if let strongSelf = self { strongSelf.loadingMessage.set(true) strongSelf.messageIndexDisposable.set((searchMessageIdByTimestamp(account: strongSelf.account, peerId: peerId, timestamp: timestamp) |> deliverOnMainQueue).start(next: { messageId in if let strongSelf = self { strongSelf.loadingMessage.set(false) if let messageId = messageId { strongSelf.navigateToMessage(from: nil, to: .id(messageId)) } } })) } }) strongSelf.present(controller, in: .window(.root)) } }, toggleMembersSearch: { [weak self] value in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in if value { return state.updatedSearch(ChatSearchData(query: "", domain: .members, domainSuggestionContext: .none, resultsState: nil)) } else if let search = state.search { switch search.domain { case .everything: return state case .members: return state.updatedSearch(ChatSearchData(query: "", domain: .everything, domainSuggestionContext: .none, resultsState: nil)) case .member: return state.updatedSearch(ChatSearchData(query: "", domain: .members, domainSuggestionContext: .none, resultsState: nil)) } } else { return state } }) } }, navigateToMessage: { [weak self] messageId in self?.navigateToMessage(from: nil, to: .id(messageId)) }, openPeerInfo: { [weak self] in self?.navigationButtonAction(.openChatInfo) }, togglePeerNotifications: { [weak self] in if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { let _ = togglePeerMuted(account: strongSelf.account, peerId: peerId).start() } }, sendContextResult: { [weak self] results, result in self?.enqueueChatContextResult(results, result) }, sendBotCommand: { [weak self] botPeer, command in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let addressName = botPeer.addressName { let messageText: String if peer is TelegramUser { messageText = command } else { messageText = command + "@" + addressName } let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } }) } }) var attributes: [MessageAttribute] = [] let entities = generateTextEntities(messageText, enabledTypes: .all) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } strongSelf.sendMessages([.message(text: messageText, attributes: attributes, mediaReference: nil, replyToMessageId: replyMessageId, localGroupingKey: nil)]) } } }, sendBotStart: { [weak self] payload in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { strongSelf.startBot(payload) } }, botSwitchChatWithPayload: { [weak self] peerId, payload in if let strongSelf = self, case let .peer(currentPeerId) = strongSelf.chatLocation { strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .automatic(returnToPeerId: currentPeerId))), fromMessage: nil) } }, beginMediaRecording: { [weak self] isVideo in guard let strongSelf = self else { return } let requestId = strongSelf.beginMediaRecordingRequestId let begin: () -> Void = { guard let strongSelf = self, strongSelf.beginMediaRecordingRequestId == requestId else { return } let hasOngoingCall: Signal if let signal = strongSelf.account.telegramApplicationContext.hasOngoingCall { hasOngoingCall = signal } else { hasOngoingCall = .single(false) } let _ = (hasOngoingCall |> deliverOnMainQueue).start(next: { hasOngoingCall in guard let strongSelf = self, strongSelf.beginMediaRecordingRequestId == requestId else { return } if hasOngoingCall { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.Call_CallInProgressTitle, text: strongSelf.presentationData.strings.Call_RecordingDisabledMessage, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { })]), in: .window(.root)) } else { if isVideo { strongSelf.requestVideoRecorder() } else { strongSelf.requestAudioRecorder(beginWithTone: false) } } }) } DeviceAccess.authorizeAccess(to: .microphone(isVideo ? .video : .audio), presentationData: strongSelf.presentationData, present: { c, a in self?.present(c, in: .window(.root), with: a) }, openSettings: { self?.account.telegramApplicationContext.applicationBindings.openSettings() }, { granted in guard let strongSelf = self, granted else { return } if isVideo { DeviceAccess.authorizeAccess(to: .camera, presentationData: strongSelf.presentationData, present: { c, a in self?.present(c, in: .window(.root), with: a) }, openSettings: { self?.account.telegramApplicationContext.applicationBindings.openSettings() }, { granted in if granted { begin() } }) } else { begin() } }) }, finishMediaRecording: { [weak self] action in guard let strongSelf = self else { return } strongSelf.beginMediaRecordingRequestId += 1 strongSelf.dismissMediaRecorder(action) }, stopMediaRecording: { [weak self] in guard let strongSelf = self else { return } strongSelf.beginMediaRecordingRequestId += 1 self?.stopMediaRecorder() }, lockMediaRecording: { [weak self] in self?.lockMediaRecorder() }, deleteRecordedMedia: { [weak self] in self?.deleteMediaRecording() }, sendRecordedMedia: { [weak self] in self?.sendMediaRecording() }, displayRestrictedInfo: { [weak self] subject in if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let bannedRights = (peer as? TelegramChannel)?.bannedRights { let banDescription: String switch subject { case .stickers: banDescription = strongSelf.presentationInterfaceState.strings.Group_ErrorSendRestrictedStickers case .mediaRecording: if bannedRights.untilDate != 0 && bannedRights.untilDate != Int32.max { banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: bannedRights.untilDate, strings: strongSelf.presentationInterfaceState.strings, dateTimeFormat: strongSelf.presentationInterfaceState.dateTimeFormat)).0 } else { banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia } } if strongSelf.recordingModeFeedback == nil { strongSelf.recordingModeFeedback = HapticFeedback() strongSelf.recordingModeFeedback?.prepareError() } strongSelf.recordingModeFeedback?.error() let rect: CGRect? switch subject { case .stickers: rect = strongSelf.chatDisplayNode.frameForStickersButton() case .mediaRecording: rect = strongSelf.chatDisplayNode.frameForInputActionButton() } if let tooltipController = strongSelf.mediaRecordingModeTooltipController { tooltipController.text = banDescription } else if let rect = rect { let tooltipController = TooltipController(text: banDescription) strongSelf.mediaRecordingModeTooltipController = tooltipController tooltipController.dismissed = { [weak tooltipController] in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRecordingModeTooltipController === tooltipController { strongSelf.mediaRecordingModeTooltipController = nil } } strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { if let strongSelf = self { return (strongSelf.chatDisplayNode, rect) } return nil })) } } }, switchMediaRecordingMode: { [weak self] in if let strongSelf = self { if strongSelf.recordingModeFeedback == nil { strongSelf.recordingModeFeedback = HapticFeedback() strongSelf.recordingModeFeedback?.prepareImpact() } strongSelf.recordingModeFeedback?.impact() var updatedMode: ChatTextInputMediaRecordingButtonMode? strongSelf.updateChatPresentationInterfaceState(interactive: true, { return $0.updatedInterfaceState { current in let mode: ChatTextInputMediaRecordingButtonMode switch current.mediaRecordingMode { case .audio: mode = .video case .video: mode = .audio } updatedMode = mode return current.withUpdatedMediaRecordingMode(mode) } }) if let updatedMode = updatedMode, updatedMode == .video { let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(postbox: strongSelf.account.postbox, count: 3).start() } strongSelf.displayMediaRecordingTip() } }, setupMessageAutoremoveTimeout: { [weak self] in if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { strongSelf.chatDisplayNode.dismissInput() if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat { let controller = ChatSecretAutoremoveTimerActionSheetController(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, currentValue: peer.messageAutoremoveTimeout == nil ? 0 : peer.messageAutoremoveTimeout!, applyValue: { value in if let strongSelf = self { let _ = setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.account, peerId: peer.id, timeout: value == 0 ? nil : value).start() } }) strongSelf.present(controller, in: .window(.root)) } } }, sendSticker: { [weak self] file in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) }.updatedInputMode { current in if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil { return .media(mode: mode, expanded: nil) } return current } }) } }) strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: file.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, unblockPeer: { [weak self] in self?.unblockPeer() }, pinMessage: { [weak self] messageId in if let strongSelf = self, case let .peer(currentPeerId) = strongSelf.chatLocation { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { var canManagePin = false if let channel = peer as? TelegramChannel { if case .broadcast = channel.info { canManagePin = channel.hasAdminRights([.canEditMessages]) } else { canManagePin = channel.hasAdminRights([.canPinMessages]) } } else if let group = peer as? TelegramGroup { if group.flags.contains(.adminsEnabled) { switch group.role { case .creator, .admin: canManagePin = true default: canManagePin = false } } else { canManagePin = true } } else if let _ = peer as? TelegramUser, strongSelf.presentationInterfaceState.explicitelyCanPinMessages { canManagePin = true } if canManagePin { let pinAction: (Bool) -> Void = { notify in if let strongSelf = self { let disposable: MetaDisposable if let current = strongSelf.unpinMessageDisposable { disposable = current } else { disposable = MetaDisposable() strongSelf.unpinMessageDisposable = disposable } disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: currentPeerId, update: .pin(id: messageId, silent: !notify)).start()) } } var pinImmediately = false if let channel = peer as? TelegramChannel, case .broadcast = channel.info { pinImmediately = true } else if let _ = peer as? TelegramUser { pinImmediately = true } if pinImmediately { pinAction(true) } else { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_PinMessageAlertGroup, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_PinMessageAlert_OnlyPin, action: { pinAction(false) }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { pinAction(true) })]), in: .window(.root)) } } else { if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessage?.id { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedPinnedMessageId(pinnedMessageId) }) }) }) } } } } }, unpinMessage: { [weak self] in if let strongSelf = self { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { var canManagePin = false if let channel = peer as? TelegramChannel { if case .broadcast = channel.info { canManagePin = channel.hasAdminRights([.canEditMessages]) } else { canManagePin = channel.hasAdminRights([.canPinMessages]) } } else if let group = peer as? TelegramGroup { if group.flags.contains(.adminsEnabled) { switch group.role { case .creator, .admin: canManagePin = true default: canManagePin = false } } else { canManagePin = true } } else if let _ = peer as? TelegramUser, strongSelf.presentationInterfaceState.explicitelyCanPinMessages { canManagePin = true } if canManagePin { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Yes, action: { if let strongSelf = self { let disposable: MetaDisposable if let current = strongSelf.unpinMessageDisposable { disposable = current } else { disposable = MetaDisposable() strongSelf.unpinMessageDisposable = disposable } disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: peer.id, update: .clear).start()) } })]), in: .window(.root)) } else { if let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedPinnedMessageId(pinnedMessage.id) }) }) }) } } } } }, reportPeer: { [weak self] in self?.reportPeer() }, presentPeerContact: { [weak self] in self?.addPeerContact() }, dismissReportPeer: { [weak self] in self?.dismissReportPeer() }, deleteChat: { [weak self] in self?.deleteChat(reportChatSpam: false) }, beginCall: { [weak self] in if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { strongSelf.controllerInteraction?.callPeer(peerId) } }, toggleMessageStickerStarred: { [weak self] messageId in if let strongSelf = self, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { var stickerFile: TelegramMediaFile? for media in message.media { if let file = media as? TelegramMediaFile, file.isSticker { stickerFile = file } } if let stickerFile = stickerFile { let postbox = strongSelf.account.postbox let network = strongSelf.account.network let _ = (strongSelf.account.postbox.transaction { transaction -> Signal in if getIsStickerSaved(transaction: transaction, fileId: stickerFile.fileId) { removeSavedSticker(transaction: transaction, mediaId: stickerFile.fileId) return .complete() } else { return addSavedSticker(postbox: postbox, network: network, file: stickerFile) |> `catch` { _ -> Signal in return .complete() } } } |> switchToLatest).start() } } }, presentController: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) }, getNavigationController: { [weak self] in return self?.navigationController as? NavigationController }, presentGlobalOverlayController: { [weak self] controller, arguments in self?.presentInGlobalOverlay(controller, with: arguments) }, navigateFeed: { [weak self] in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.scrollToNextMessage() } }, openGrouping: { [weak self] in if let strongSelf = self, case let .group(groupId) = strongSelf.chatLocation { (strongSelf.navigationController as? NavigationController)?.pushViewController(FeedGroupingController(account: strongSelf.account, groupId: groupId)) } }, toggleSilentPost: { [weak self] in if let strongSelf = self { var value: Bool = false strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedInterfaceState { value = !$0.silentPosting return $0.withUpdatedSilentPosting(value) } }) var rect: CGRect? = strongSelf.chatDisplayNode.frameForInputPanelAccessoryButton(.silentPost(true)) if rect == nil { rect = strongSelf.chatDisplayNode.frameForInputPanelAccessoryButton(.silentPost(false)) } let text: String if !value { text = strongSelf.presentationData.strings.Conversation_SilentBroadcastTooltipOn } else { text = strongSelf.presentationData.strings.Conversation_SilentBroadcastTooltipOff } if let tooltipController = strongSelf.silentPostTooltipController { tooltipController.text = text } else if let rect = rect { let tooltipController = TooltipController(text: text) strongSelf.silentPostTooltipController = tooltipController tooltipController.dismissed = { [weak tooltipController] in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.silentPostTooltipController === tooltipController { strongSelf.silentPostTooltipController = nil } } strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { if let strongSelf = self { return (strongSelf.chatDisplayNode, rect) } return nil })) } } }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get())) switch self.chatLocation { case let .peer(peerId): let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(peerId), .total(ApplicationSpecificPreferencesKeys.inAppNotificationSettings)]) let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerIds: Set([peerId])) self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey, notificationSettingsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in if let strongSelf = self { var unreadCount: Int32 = 0 var totalChatCount: Int32 = 0 var inAppSettingsValue: InAppNotificationSettings? if let view = views.views[unreadCountsKey] as? UnreadMessageCountsView { if let count = view.count(for: .peer(peerId)) { unreadCount = count } if let (preferencesEntry, state) = view.total() { let inAppSettings = (preferencesEntry as? InAppNotificationSettings) ?? InAppNotificationSettings.defaultSettings inAppSettingsValue = inAppSettings let (count, _) = renderedTotalUnreadCount(inAppSettings: inAppSettings, totalUnreadState: state) totalChatCount = count } } strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount if let view = views.views[notificationSettingsKey] as? PeerNotificationSettingsView, let notificationSettings = view.notificationSettings[peerId] { var globalRemainingUnreadChatCount = totalChatCount if !notificationSettings.isRemovedFromTotalUnreadCount && unreadCount > 0 { let inAppSettings = inAppSettingsValue ?? InAppNotificationSettings.defaultSettings if case .messages = inAppSettings.totalUnreadCountDisplayCategory { globalRemainingUnreadChatCount -= unreadCount } else { globalRemainingUnreadChatCount -= 1 } } if globalRemainingUnreadChatCount > 0 { strongSelf.navigationItem.badge = "\(globalRemainingUnreadChatCount)" } else { strongSelf.navigationItem.badge = "" } } } }) self.chatUnreadMentionCountDisposable = (self.account.viewTracker.unseenPersonalMessagesCount(peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] count in if let strongSelf = self { strongSelf.chatDisplayNode.navigateButtons.mentionCount = count } }) let postbox = self.account.postbox let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) self.peerInputActivitiesDisposable = (self.account.peerInputActivities(peerId: peerId) |> mapToSignal { activities -> Signal<[(Peer, PeerInputActivity)], NoError> in var foundAllPeers = true var cachedResult: [(Peer, PeerInputActivity)] = [] previousPeerCache.with { dict -> Void in for (peerId, activity) in activities { if let peer = dict[peerId] { cachedResult.append((peer, activity)) } else { foundAllPeers = false break } } } if foundAllPeers { return .single(cachedResult) } else { return postbox.transaction { transaction -> [(Peer, PeerInputActivity)] in var result: [(Peer, PeerInputActivity)] = [] var peerCache: [PeerId: Peer] = [:] for (peerId, activity) in activities { if let peer = transaction.getPeer(peerId) { result.append((peer, activity)) peerCache[peerId] = peer } } let _ = previousPeerCache.swap(peerCache) return result } } } |> deliverOnMainQueue).start(next: { [weak self] activities in if let strongSelf = self { strongSelf.chatTitleView?.inputActivities = (peerId, activities) } }) self.sentMessageEventsDisposable.set(self.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId).start(next: { [weak self] _ in if let strongSelf = self { let inAppNotificationSettings: InAppNotificationSettings = strongSelf.account.telegramApplicationContext.currentInAppNotificationSettings.with { $0 } if inAppNotificationSettings.playSounds { serviceSoundManager.playMessageDeliveredSound() } } })) self.failedMessageEventsDisposable.set((self.account.pendingMessageManager.failedMessageEvents(peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] reason in if let strongSelf = self { let text: String switch reason { case .flood: text = strongSelf.presentationData.strings.Conversation_SendMessageErrorFlood case .publicBan: text = strongSelf.presentationData.strings.Conversation_SendMessageErrorGroupRestricted } strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Generic_ErrorMoreInfo, action: { self?.openPeerMention("spambot", navigation: .chat(textInputState: nil, messageId: nil)) }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) case let .group(groupId): let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.group(groupId), .total(ApplicationSpecificPreferencesKeys.inAppNotificationSettings)]) self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in if let strongSelf = self { var unreadCount: Int32 = 0 var totalCount: Int32 = 0 if let view = views.views[unreadCountsKey] as? UnreadMessageCountsView { if let count = view.count(for: .group(groupId)) { unreadCount = count } if let (preferencesEntry, state) = view.total() { let inAppSettings = (preferencesEntry as? InAppNotificationSettings) ?? InAppNotificationSettings.defaultSettings let (count, _) = renderedTotalUnreadCount(inAppSettings: inAppSettings, totalUnreadState: state) totalCount = count } } strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount } }) } self.interfaceInteraction = interfaceInteraction self.chatDisplayNode.interfaceInteraction = interfaceInteraction if let mediaManager = self.account.telegramApplicationContext.mediaManager { self.galleryHiddenMesageAndMediaDisposable.set(mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { var messageIdAndMedia: [MessageId: [Media]] = [:] for id in ids { if case let .chat(messageId, media) = id { messageIdAndMedia[messageId] = [media] } } //if controllerInteraction.hiddenMedia != messageIdAndMedia { controllerInteraction.hiddenMedia = messageIdAndMedia strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { itemNode.updateHiddenMedia() } } //} } })) } self.chatDisplayNode.dismissAsOverlay = { [weak self] in if let strongSelf = self { strongSelf.chatDisplayNode.animateDismissAsOverlay(completion: { self?.presentingViewController?.dismiss(animated: false, completion: nil) }) } } self.displayNodeDidLoad() } override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.chatDisplayNode.historyNode.preloadPages = true self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest((self.account.applicationContext as! TelegramApplicationContext).applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in return a && b }) self.chatDisplayNode.loadInputPanels(theme: self.presentationInterfaceState.theme, strings: self.presentationInterfaceState.strings) self.recentlyUsedInlineBotsDisposable = (recentlyUsedInlineBots(postbox: self.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] peers in self?.recentlyUsedInlineBotsValue = peers.filter({ $0.1 >= 0.14 }).map({ $0.0 }) }) if case .standard(false) = 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.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { return false } if !strongSelf.traceVisibility() { return false } if !isTopmostChatController(strongSelf) { return false } if strongSelf.firstLoadedMessageToListen() != nil || strongSelf.chatDisplayNode.isTextInputPanelActive { if strongSelf.account.telegramApplicationContext.immediateHasOngoingCall { return false } if case let .media(_, expanded) = strongSelf.presentationInterfaceState.inputMode, expanded != nil { return false } if !strongSelf.account.telegramApplicationContext.currentMediaInputSettings.with { $0.enableRaiseToSpeak } { 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 if let strongSelf = self, let raiseToListen = strongSelf.raiseToListen { strongSelf.voicePlaylistDidEndTimestamp = CACurrentMediaTime() raiseToListen.activateBasedOnProximity(delay: 0.0) } } self.tempVoicePlaylistItemChanged = { [weak self] previousItem, currentItem in guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else { return } if let currentItem = currentItem?.id as? PeerMessagesMediaPlaylistItemId, let previousItem = previousItem?.id as? PeerMessagesMediaPlaylistItemId, previousItem.messageId.peerId == peerId, currentItem.messageId.peerId == peerId, currentItem.messageId != previousItem.messageId { if strongSelf.chatDisplayNode.historyNode.isMessageVisibleOnScreen(currentItem.messageId) { strongSelf.navigateToMessage(from: nil, to: .id(currentItem.messageId), scrollPosition: .center(.bottom), rememberInStack: false, animated: true, completion: nil) } } } } 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 9.0, *) { //self.registerForPreviewing(with: self, sourceView: self.chatDisplayNode.historyNodeContainer.view, theme: PeekControllerTheme(presentationTheme: self.presentationData.theme), onlyNative: true) if case .peer = self.chatLocation, let buttonView = (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.view { //self.registerForPreviewing(with: self, sourceView: buttonView, theme: PeekControllerTheme(presentationTheme: self.presentationData.theme), onlyNative: true) } } if #available(iOSApplicationExtension 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 { let _ = checkPeerChatServiceActions(postbox: self.account.postbox, peerId: peerId).start() } if self.chatDisplayNode.frameForInputActionButton() != nil, self.presentationInterfaceState.interfaceState.mediaRecordingMode == .audio { let _ = (ApplicationSpecificNotice.getChatMediaMediaRecordingTips(postbox: self.account.postbox) |> deliverOnMainQueue).start(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(postbox: strongSelf.account.postbox).start() strongSelf.displayMediaRecordingTip() } }) } } } override public func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.chatDisplayNode.historyNode.canReadHistory.set(.single(false)) self.saveInterfaceState() self.silentPostTooltipController?.dismiss() self.mediaRecordingModeTooltipController?.dismiss() } private func saveInterfaceState() { if case let .peer(peerId) = self.chatLocation { let timestamp = Int32(Date().timeIntervalSince1970) let scrollState = self.chatDisplayNode.historyNode.immediateScrollState() let interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp).withUpdatedHistoryScrollState(scrollState) let _ = updatePeerChatInterfaceState(account: account, peerId: peerId, state: interfaceState).start() } } override public func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedTitlePanelContext { if let index = $0.index(where: { switch $0 { case .chatInfo: return true default: return false } }) { var updatedContexts = $0 updatedContexts.remove(at: index) return updatedContexts } else { return $0 } } }) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.validLayout = layout self.chatTitleView?.layout = layout self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop in self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop) }) } func updateChatPresentationInterfaceState(animated: Bool = true, interactive: Bool, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) { self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interactive: interactive, f) } func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, interactive: Bool, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) { var temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState) if self.presentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup { if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, let _ = keyboardButtonsMessage.visibleButtonKeyboardMarkup { if self.presentationInterfaceState.interfaceState.editMessage == nil && self.presentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId && temporaryChatPresentationInterfaceState.botStartPayload == nil { temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ _ in return .inputButtons }) } if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup { if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ $0.withUpdatedProcessedSetupReplyMessageId(keyboardButtonsMessage.id) }) }) } } } else { temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ mode in if case .inputButtons = mode { return .text } else { return mode } }) } } if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, keyboardButtonsMessage.requestsSetupReply { if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ $0.withUpdatedProcessedSetupReplyMessageId(keyboardButtonsMessage.id) }) }) } } let inputTextPanelState = inputTextPanelStateForChatPresentationInterfaceState(temporaryChatPresentationInterfaceState, account: self.account) var updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputTextPanelState({ _ in return inputTextPanelState }) let contextQueryUpdates = contextQueryResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, account: self.account, currentQueryStates: &self.contextQueryStates) for (kind, update) in contextQueryUpdates { switch update { case .remove: if let (_, disposable) = self.contextQueryStates[kind] { disposable.dispose() self.contextQueryStates.removeValue(forKey: kind) updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { _ in return nil }) } case let .update(query, signal): let currentQueryAndDisposable = self.contextQueryStates[kind] currentQueryAndDisposable?.1.dispose() var inScope = true var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? self.contextQueryStates[kind] = (query, (signal |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { if Thread.isMainThread && inScope { inScope = false inScopeResult = result } else { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInputQueryResult(queryKind: kind, { previousResult in return result(previousResult) }) }) } } })) inScope = false if let inScopeResult = inScopeResult { updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { previousResult in return inScopeResult(previousResult) }) } if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { if case .contextRequest = query { let _ = (ApplicationSpecificNotice.getSecretChatInlineBotUsage(postbox: self.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self, !value { let _ = ApplicationSpecificNotice.setSecretChatInlineBotUsage(postbox: strongSelf.account.postbox).start() strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_SecretChatContextBotAlert, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } }) } } } } if let (updatedSearchQuerySuggestionState, updatedSearchQuerySuggestionSignal) = searchQuerySuggestionResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, account: self.account, currentQuery: self.searchQuerySuggestionState?.0) { self.searchQuerySuggestionState?.1.dispose() var inScope = true var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? self.searchQuerySuggestionState = (updatedSearchQuerySuggestionState, (updatedSearchQuerySuggestionSignal |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { if Thread.isMainThread && inScope { inScope = false inScopeResult = result } else { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedSearchQuerySuggestionResult { previousResult in return result(previousResult) } }) } } })) inScope = false if let inScopeResult = inScopeResult { updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedSearchQuerySuggestionResult { previousResult in return inScopeResult(previousResult) } } } if let (updatedUrlPreviewUrl, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText.string, account: self.account, currentQuery: self.urlPreviewQueryState?.0) { self.urlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? let linkPreviews: Signal if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { linkPreviews = interactiveChatLinkPreviewsEnabled(postbox: self.account.postbox, displayAlert: { [weak self] f in if let strongSelf = self { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_SecretLinkPreviewAlert, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { f.f(true) }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_No, action: { f.f(false) })]), in: .window(.root)) } }) } else { if let bannedRights = (self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel)?.bannedRights, bannedRights.flags.contains(.banEmbedLinks) { linkPreviews = .single(false) } else { linkPreviews = .single(true) } } let filteredPreviewSignal = linkPreviews |> take(1) |> mapToSignal { value -> Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError> in if value { return updatedUrlPreviewSignal } else { return .single({ _ in return nil }) } } self.urlPreviewQueryState = (updatedUrlPreviewUrl, (filteredPreviewSignal |> deliverOnMainQueue).start(next: { [weak self] (result) in if let strongSelf = self { if Thread.isMainThread && inScope { inScope = false inScopeResult = result } else { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = result($0.urlPreview?.1) { return $0.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) } else { return $0.updatedUrlPreview(nil) } }) } } })) inScope = false if let inScopeResult = inScopeResult { if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.1) { updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) } else { updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(nil) } } } let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.content != .plaintext let editingUrlPreviewText: String? = isEditingMedia ? nil : updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText.string if let (updatedEditingUrlPreviewUrl, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, account: self.account, currentQuery: self.editingUrlPreviewQueryState?.0) { self.editingUrlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? self.editingUrlPreviewQueryState = (updatedEditingUrlPreviewUrl, (updatedEditingUrlPreviewSignal |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { if Thread.isMainThread && inScope { inScope = false inScopeResult = result } else { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = result($0.editingUrlPreview?.1) { return $0.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage)) } else { return $0.updatedEditingUrlPreview(nil) } }) } } })) inScope = false if let inScopeResult = inScopeResult { if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.1) { updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage)) } else { updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(nil) } } } if let updated = self.updateSearch(updatedChatPresentationInterfaceState) { updatedChatPresentationInterfaceState = updated } let recordingActivityValue: ChatRecordingActivity if let mediaRecordingState = updatedChatPresentationInterfaceState.inputTextPanelState.mediaRecordingState { switch mediaRecordingState { case .audio: recordingActivityValue = .voice case .video(ChatVideoRecordingStatus.recording, _): recordingActivityValue = .instantVideo default: recordingActivityValue = .none } } else { recordingActivityValue = .none } if recordingActivityValue != self.recordingActivityValue { self.recordingActivityValue = recordingActivityValue self.recordingActivityPromise.set(recordingActivityValue) } self.presentationInterfaceState = updatedChatPresentationInterfaceState if self.isNodeLoaded { self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, transition: transition, interactive: interactive) } if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { if self.leftNavigationButton != button { self.navigationItem.setLeftBarButton(button.buttonItem, animated: transition.isAnimated) self.leftNavigationButton = button } } else if let _ = self.leftNavigationButton { self.navigationItem.setLeftBarButton(nil, animated: transition.isAnimated) self.leftNavigationButton = nil } if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) { if self.rightNavigationButton != button { self.navigationItem.setRightBarButton(button.buttonItem, animated: transition.isAnimated) self.rightNavigationButton = button } } else if let _ = self.rightNavigationButton { self.navigationItem.setRightBarButton(nil, animated: transition.isAnimated) self.rightNavigationButton = nil } if let controllerInteraction = self.controllerInteraction { if updatedChatPresentationInterfaceState.interfaceState.selectionState != controllerInteraction.selectionState { controllerInteraction.selectionState = updatedChatPresentationInterfaceState.interfaceState.selectionState self.updateItemNodesSelectionStates(animated: transition.isAnimated) } } switch updatedChatPresentationInterfaceState.mode { case .standard: self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style self.deferScreenEdgeGestures = [] case .overlay: self.statusBar.statusBarStyle = .Hide self.deferScreenEdgeGestures = [.top] case .inline: self.statusBar.statusBarStyle = .Ignore } } private func updateItemNodesSelectionStates(animated: Bool) { self.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { itemNode.updateSelectionState(animated: animated) } } } private 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 { self.navigationButtonAction(button.action) } } private func navigationButtonAction(_ action: ChatNavigationButtonAction) { switch action { case .cancelMessageSelection: self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) case .clearHistory: if case let .peer(peerId) = self.chatLocation { let text: String if peerId == account.peerId { text = self.presentationData.strings.Conversation_ClearSelfHistory } else if peerId.namespace == Namespaces.Peer.SecretChat { text = self.presentationData.strings.Conversation_ClearSecretHistory } else if peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel { text = self.presentationData.strings.Conversation_ClearGroupHistory } else { text = self.presentationData.strings.Conversation_ClearPrivateHistory } let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: text), ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ClearAll, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) let _ = clearHistoryInteractively(postbox: strongSelf.account.postbox, peerId: peerId).start() } }) ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) self.chatDisplayNode.dismissInput() self.present(actionSheet, in: .window(.root)) } case .openChatInfo: switch self.chatLocationInfoData { case let .peer(peerView): self.navigationActionDisposable.set((peerView.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerView in if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText == nil { if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } } })) case .group: if case let .group(groupId) = self.chatLocation { (self.navigationController as? NavigationController)?.pushViewController(ChatListController(account: self.account, groupId: groupId, controlsHistoryPreload: false)) } } case .search: self.interfaceInteraction?.beginMessageSearch(.everything, "") } } private func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) { if let message = messages.first, case let .message(desc) = message, let mediaReference = desc.mediaReference { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in 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 !desc.text.isEmpty { state = state.updatedInterfaceState { state in if let editMessage = state.editMessage { return state.withUpdatedEditMessage(ChatEditMessageState(messageId: editMessage.messageId, inputState: ChatTextInputState(inputText: NSAttributedString(string: desc.text)), disableUrlPreview: editMessage.disableUrlPreview)) } return state } } return state }) } } private func editMessageMediaWithLegacySignals(_ signals: [Any]) { guard case .peer = self.chatLocation else { return } let _ = (legacyAssetPickerEnqueueMessages(account: self.account, signals: signals) |> deliverOnMainQueue).start(next: { [weak self] messages in self?.editMessageMediaWithMessages(messages) }) } private func presentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?) { let _ = (self.account.postbox.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings return entry ?? GeneratedMediaStoreSettings.defaultSettings } |> deliverOnMainQueue).start(next: { [weak self] settings in guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { return } strongSelf.chatDisplayNode.dismissInput() if editMediaOptions == nil, let bannedRights = (peer as? TelegramChannel)?.bannedRights, bannedRights.flags.contains(.banSendMedia) { let banDescription: String if bannedRights.untilDate != 0 && bannedRights.untilDate != Int32.max { banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: bannedRights.untilDate, strings: strongSelf.presentationInterfaceState.strings, dateTimeFormat: strongSelf.presentationInterfaceState.dateTimeFormat)).0 } else { banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia } let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: banDescription), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Location, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() self?.presentMapPicker(editingMessage: false) }), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Contact, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() self?.presentContactPicker() }) ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.present(actionSheet, in: .window(.root)) return } let legacyController = LegacyController(presentation: .custom, theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) legacyController.statusBar.statusBarStyle = .Ignore legacyController.controllerLoaded = { [weak legacyController] in legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true } let emptyController = LegacyEmptyController(context: legacyController.context)! let navigationController = makeLegacyNavigationController(rootController: emptyController) navigationController.setNavigationBarHidden(true, animated: false) legacyController.bind(controller: navigationController) legacyController.enableSizeClassSignal = true let controller = legacyAttachmentMenu(account: strongSelf.account, peer: peer, editMediaOptions: editMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, openGallery: { self?.presentMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals in if editMediaOptions != nil { self?.editMessageMediaWithLegacySignals(signals) } else { self?.enqueueMediaMessages(signals: signals) } }) }, openCamera: { cameraView, menuController in if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { presentedLegacyCamera(account: strongSelf.account, peer: peer, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: settings.storeEditedPhotos, mediaGrouping: true, sendMessagesWithSignals: { signals in if editMediaOptions != nil { self?.editMessageMediaWithLegacySignals(signals!) } else { self?.enqueueMediaMessages(signals: signals) } }) } }, openFileGallery: { self?.presentFileMediaPickerOptions(editingMessage: editMediaOptions != nil) }, openMap: { self?.presentMapPicker(editingMessage: editMediaOptions != nil) }, openContacts: { self?.presentContactPicker() }, sendMessagesWithSignals: { [weak self] signals in if editMediaOptions != nil { self?.editMessageMediaWithLegacySignals(signals!) } else { self?.enqueueMediaMessages(signals: signals) } }, selectRecentlyUsedInlineBot: { [weak self] peer in if let strongSelf = self, let addressName = peer.addressName { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in return .text }) }) } }) controller.didDismiss = { [weak legacyController] _ in legacyController?.dismiss() } controller.customRemoveFromParentViewController = { [weak legacyController] in legacyController?.dismiss() } strongSelf.present(legacyController, in: .window(.root)) controller.present(in: emptyController, sourceView: nil, animated: true) }) } private func presentFileMediaPickerOptions(editingMessage: Bool) { let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FilePhotoOrVideo, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.presentMediaPicker(fileMode: true, editingMedia: editingMessage, completion: { signals in if editingMessage { self?.editMessageMediaWithLegacySignals(signals) } else { self?.enqueueMediaMessages(signals: signals) } }) } }), ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FileICloudDrive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.present(legacyICloudFileController(theme: strongSelf.presentationData.theme, completion: { urls in if let strongSelf = self, !urls.isEmpty { var signals: [Signal] = [] for url in urls { signals.append(iCloudFileDescription(url)) } strongSelf.enqueueMediaMessageDisposable.set((combineLatest(signals) |> deliverOnMainQueue).start(next: { results in if let strongSelf = self { let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId var messages: [EnqueueMessage] = [] for item in results { if let item = item { let fileId = arc4random64() let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData), previewRepresentations: [], mimeType: guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension), size: item.fileSize, attributes: [.FileName(fileName: item.fileName)]) let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: nil) messages.append(message) } } if !messages.isEmpty { if editingMessage { strongSelf.editMessageMediaWithMessages(messages) } else { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) strongSelf.sendMessages(messages) } } } })) } }), in: .window(.root)) } }) ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) self.chatDisplayNode.dismissInput() self.present(actionSheet, in: .window(.root)) } private func presentMediaPicker(fileMode: Bool, editingMedia: Bool, completion: @escaping ([Any]) -> Void) { let _ = (self.account.postbox.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings return entry ?? GeneratedMediaStoreSettings.defaultSettings } |> deliverOnMainQueue).start(next: { [weak self] settings in guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { return } let _ = legacyAssetPicker(applicationContext: strongSelf.account.telegramApplicationContext, presentationData: strongSelf.presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true).start(next: { generator in if let strongSelf = self { let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) legacyController.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBar.style.style legacyController.controllerLoaded = { [weak legacyController] in legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true } let controller = generator(legacyController.context) legacyController.bind(controller: controller) legacyController.deferScreenEdgeGestures = [.top] configureLegacyAssetPicker(controller, account: strongSelf.account, peer: peer) controller.descriptionGenerator = legacyAssetPickerItemGenerator() controller.completionBlock = { [weak legacyController] signals in if let legacyController = legacyController { legacyController.dismiss() completion(signals!) } } controller.dismissalBlock = { [weak legacyController] in if let legacyController = legacyController { legacyController.dismiss() } } strongSelf.chatDisplayNode.dismissInput() strongSelf.present(legacyController, in: .window(.root)) } }) }) } private func presentMapPicker(editingMessage: Bool) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } let selfPeerId: PeerId if let peer = peer as? TelegramChannel, case .broadcast = peer.info { selfPeerId = peer.id } else { selfPeerId = self.account.peerId } let _ = (self.account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(selfPeerId) } |> deliverOnMainQueue).start(next: { [weak self] selfPeer in guard let strongSelf = self, let selfPeer = selfPeer else { return } strongSelf.chatDisplayNode.dismissInput() strongSelf.present(legacyLocationPickerController(account: strongSelf.account, selfPeer: selfPeer, peer: peer, sendLocation: { coordinate, venue in guard let strongSelf = self else { return } let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil)), replyToMessageId: replyMessageId, localGroupingKey: nil) if editingMessage { strongSelf.editMessageMediaWithMessages([message]) } else { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) strongSelf.sendMessages([message]) } }, sendLiveLocation: { [weak self] coordinate, period in guard let strongSelf = self else { return } let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period)), replyToMessageId: replyMessageId, localGroupingKey: nil) if editingMessage { strongSelf.editMessageMediaWithMessages([message]) } else { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) strongSelf.sendMessages([message]) } }, theme: strongSelf.presentationData.theme), in: .window(.root)) }) } private func presentContactPicker() { let contactsController = ContactSelectionController(account: self.account, title: { $0.Contacts_Title }, displayDeviceContacts: true) self.chatDisplayNode.dismissInput() self.present(contactsController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) self.controllerNavigationDisposable.set((contactsController.result |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, let peer = peer { let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> switch peer { case let .peer(contact, _): guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { return } let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []) let account = strongSelf.account dataSignal = strongSelf.account.telegramApplicationContext.contactDataManager.basicData() |> take(1) |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in var stableId: String? let queryPhoneNumber = formatPhoneNumber(phoneNumber) outer: for (id, data) in basicData { for phoneNumber in data.phoneNumbers { if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber { stableId = id break outer } } } if let stableId = stableId { return account.telegramApplicationContext.contactDataManager.extendedData(stableId: stableId) |> take(1) |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in return (contact, extendedData) } } else { return .single((contact, contactData)) } } case let .deviceContact(id, _): dataSignal = strongSelf.account.telegramApplicationContext.contactDataManager.extendedData(stableId: id) |> take(1) |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in return (nil, extendedData) } } strongSelf.controllerNavigationDisposable.set((dataSignal |> deliverOnMainQueue).start(next: { peerAndContactData in if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 { if contactData.isPrimitive { let phone = contactData.basicData.phoneNumbers[0].value let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil) strongSelf.sendMessages([message]) } else { strongSelf.present(deviceContactInfoController(account: strongSelf.account, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { return } let phone = contactData.basicData.phoneNumbers[0].value if let vCardData = contactData.serializedVCard() { let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil) strongSelf.sendMessages([message]) } })), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } })) } })) } private func transformEnqueueMessages(_ messages: [EnqueueMessage]) -> [EnqueueMessage] { let silentPosting = self.presentationInterfaceState.interfaceState.silentPosting return messages.map { message in if silentPosting { return message.withUpdatedAttributes { attributes in var attributes = attributes for i in 0 ..< attributes.count { if attributes[i] is NotificationInfoMessageAttribute { attributes.remove(at: i) break } } attributes.append(NotificationInfoMessageAttribute(flags: .muted)) return attributes } } else { return message } } } private func sendMessages(_ messages: [EnqueueMessage]) { if case let .peer(peerId) = self.chatLocation { self.commitPurposefulAction() let _ = (enqueueMessages(account: self.account, peerId: peerId, messages: self.transformEnqueueMessages(messages)) |> deliverOnMainQueue).start(next: { [weak self] _ in self?.chatDisplayNode.historyNode.scrollToEndOfHistory() }) } } private func enqueueMediaMessages(signals: [Any]?) { if case .peer = self.chatLocation { self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: self.account, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self] messages in if let strongSelf = self { let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }) } })) } } private func displayPasteMenu(_ images: [UIImage]) { let _ = (self.account.postbox.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings return entry ?? GeneratedMediaStoreSettings.defaultSettings } |> deliverOnMainQueue).start(next: { [weak self] settings in if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { let controller = legacyPasteMenu(account: strongSelf.account, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, images: images, sendMessagesWithSignals: { signals in self?.enqueueMediaMessages(signals: signals) }) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(controller, in: .window(.root)) } }) } private func enqueueGifData(_ data: Data) { self.enqueueMediaMessageDisposable.set((legacyEnqueueGifMessage(account: self.account, data: data) |> deliverOnMainQueue).start(next: { [weak self] message in if let strongSelf = self { let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) }) } })) } private func enqueueStickerImage(_ image: UIImage) { let size = image.size.aspectFitted(CGSize(width: 512.0, height: 512.0)) self.enqueueMediaMessageDisposable.set((convertToWebP(image: image, targetSize: size, quality: 0.85) |> deliverOnMainQueue).start(next: { [weak self] data in if let strongSelf = self, !data.isEmpty { let resource = LocalFileMediaResource(fileId: arc4random64()) strongSelf.account.postbox.mediaBox.storeResourceData(resource.id, data: data) var fileAttributes: [TelegramMediaFileAttribute] = [] fileAttributes.append(.FileName(fileName: "sticker.webp")) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) fileAttributes.append(.ImageSize(size: size)) let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: resource, previewRepresentations: [], mimeType: "image/webp", size: data.count, attributes: fileAttributes) let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil) let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) }) } })) } private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult) { guard case let .peer(peerId) = self.chatLocation else { return } if let message = outgoingMessageWithChatContextResult(to: peerId, results: results, result: result), canSendMessagesToChat(self.presentationInterfaceState) { let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } }) } }) self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageId)]) } } private 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 } private var raiseToListenActivateRecordingTimer: SwiftSignalKit.Timer? private func activateRaiseGesture() { self.raiseToListenActivateRecordingTimer?.invalidate() self.raiseToListenActivateRecordingTimer = nil if let messageToListen = self.firstLoadedMessageToListen() { let _ = self.controllerInteraction?.openMessage(messageToListen, .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() } } private func deactivateRaiseGesture() { self.raiseToListenActivateRecordingTimer?.invalidate() self.raiseToListenActivateRecordingTimer = nil self.dismissMediaRecorder(.preview) } private func requestAudioRecorder(beginWithTone: Bool) { if self.audioRecorderValue == nil { if let applicationContext = self.account.applicationContext as? TelegramApplicationContext { if self.recorderFeedback == nil { self.recorderFeedback = HapticFeedback() self.recorderFeedback?.prepareImpact(.light) } if let mediaManager = applicationContext.mediaManager { self.audioRecorder.set(mediaManager.audioRecorder(beginWithTone: beginWithTone, applicationBindings: applicationContext.applicationBindings, beganWithTone: { _ in })) } } } } private func requestVideoRecorder() { guard case let .peer(peerId) = self.chatLocation else { return } if self.videoRecorderValue == nil { if let currentInputPanelFrame = self.chatDisplayNode.currentInputPanelFrame() { if self.recorderFeedback == nil { self.recorderFeedback = HapticFeedback() self.recorderFeedback?.prepareImpact(.light) } self.videoRecorder.set(.single(legacyInstantVideoController(theme: self.presentationData.theme, panelFrame: currentInputPanelFrame, account: self.account, peerId: peerId, send: { [weak self] message in if let strongSelf = self { let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) let updatedMessage = message.withUpdatedReplyToMessageId(replyMessageId) strongSelf.sendMessages([updatedMessage]) } }))) } } } private func dismissMediaRecorder(_ action: ChatFinishMediaRecordingAction) { if let audioRecorderValue = self.audioRecorderValue { audioRecorderValue.stop() switch action { case .dismiss: break case .preview: let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in if let strongSelf = self, let data = data { if data.duration < 0.5 { strongSelf.recorderFeedback?.error() strongSelf.recorderFeedback = nil } else if let waveform = data.waveform { var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = LocalFileMediaResource(fileId: randomId, size: data.compressedData.count) strongSelf.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedRecordedMediaPreview(ChatRecordedMediaPreview(resource: resource, duration: Int32(data.duration), fileSize: Int32(data.compressedData.count), waveform: AudioWaveform(bitstream: waveform, bitsPerSample: 5))) }) strongSelf.recorderFeedback = nil } } }) case .send: let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in if let strongSelf = self, let data = data { if data.duration < 0.5 { strongSelf.recorderFeedback?.error() strongSelf.recorderFeedback = nil } else { let randomId = arc4random64() let resource = LocalFileMediaResource(fileId: randomId) strongSelf.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) var waveformBuffer: MemoryBuffer? if let waveform = data.waveform { waveformBuffer = MemoryBuffer(data: waveform) } strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) strongSelf.recorderFeedback?.tap() strongSelf.recorderFeedback = nil } } }) } self.audioRecorder.set(.single(nil)) } else if let videoRecorderValue = self.videoRecorderValue { if case .send = action { videoRecorderValue.completeVideo() //self.tempVideoRecorderValue = videoRecorderValue self.videoRecorder.set(.single(nil)) } else { self.videoRecorder.set(.single(nil)) } } } private func stopMediaRecorder() { if let audioRecorderValue = self.audioRecorderValue { if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { self.dismissMediaRecorder(.preview) } else { audioRecorderValue.stop() self.audioRecorder.set(.single(nil)) } } else if let videoRecorderValue = self.videoRecorderValue { if videoRecorderValue.stopVideo() { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInputTextPanelState { panelState in return panelState.withUpdatedMediaRecordingState(.video(status: .editing, isLocked: false)) } }) } else { self.videoRecorder.set(.single(nil)) } } } private func lockMediaRecorder() { if self.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedInputTextPanelState { panelState in return panelState.withUpdatedMediaRecordingState(panelState.mediaRecordingState?.withLocked(true)) } }) } self.videoRecorderValue?.lockVideo() } private func deleteMediaRecording() { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedRecordedMediaPreview(nil) }) } private func sendMediaRecording() { if let recordedMediaPreview = self.presentationInterfaceState.recordedMediaPreview { let waveformBuffer = MemoryBuffer(data: recordedMediaPreview.waveform.samples) self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedRecordedMediaPreview(nil).updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } }) self.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], mimeType: "audio/ogg", size: Int(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } } private func updateSearch(_ interfaceState: ChatPresentationInterfaceState) -> ChatPresentationInterfaceState? { let limit: Int32 = 100 var derivedSearchState: ChatSearchState? if let search = interfaceState.search { func loadMoreIndexFromResultsState(_ resultsState: ChatSearchResultsState?) -> MessageIndex? { guard let resultsState = resultsState, let currentId = resultsState.currentId else { return nil } if let index = resultsState.messageIndices.index(where: { $0.id == currentId }) { if index <= limit / 2 { return resultsState.messageIndices.first } } return nil } switch search.domain { case .everything: switch self.chatLocation { case let .peer(peerId): derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: nil, tags: nil), loadMoreIndex: loadMoreIndexFromResultsState(search.resultsState)) case let .group(groupId): derivedSearchState = ChatSearchState(query: search.query, location: .group(groupId), loadMoreIndex: loadMoreIndexFromResultsState(search.resultsState)) } case .members: derivedSearchState = nil case let .member(peer): switch self.chatLocation { case let .peer(peerId): derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: peer.id, tags: nil), loadMoreIndex: loadMoreIndexFromResultsState(search.resultsState)) case .group: derivedSearchState = nil } } } if derivedSearchState != self.searchState { let previousSearchState = self.searchState self.searchState = derivedSearchState if let searchState = derivedSearchState { if previousSearchState?.query != searchState.query || previousSearchState?.location != searchState.location { var queryIsEmpty = false if searchState.query.isEmpty { if case let .peer(_, fromId, _) = searchState.location { if fromId == nil { queryIsEmpty = true } } else { queryIsEmpty = true } } if queryIsEmpty { self.searching.set(false) self.searchDisposable?.set(nil) if let data = interfaceState.search { return interfaceState.updatedSearch(data.withUpdatedResultsState(nil)) } } else { self.searching.set(true) let searchDisposable: MetaDisposable if let current = self.searchDisposable { searchDisposable = current } else { searchDisposable = MetaDisposable() self.searchDisposable = searchDisposable } searchDisposable.set((searchMessages(account: self.account, location: searchState.location, query: searchState.query, limit: limit) |> map { ($0.0, $0.2) } |> delay(0.2, queue: Queue.mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] results, totalCount in guard let strongSelf = self else { return } let complete = results.count >= Int(totalCount) var navigateIndex: MessageIndex? strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search { let messageIndices = results.map({ MessageIndex($0) }).sorted() var currentIndex = messageIndices.last if let previousResultId = data.resultsState?.currentId { for index in messageIndices { if index.id >= previousResultId { currentIndex = index break } } } navigateIndex = currentIndex return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, totalCount: totalCount, complete: complete))) } else { return current } }) if let navigateIndex = navigateIndex { switch strongSelf.chatLocation { case .peer: strongSelf.navigateToMessage(from: nil, to: .id(navigateIndex.id)) case .group: strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex)) } } }, completed: { [weak self] in if let strongSelf = self { strongSelf.searching.set(false) } })) } } else if previousSearchState?.loadMoreIndex != searchState.loadMoreIndex { if let loadMoreIndex = searchState.loadMoreIndex { self.searching.set(true) let searchDisposable: MetaDisposable if let current = self.searchDisposable { searchDisposable = current } else { searchDisposable = MetaDisposable() self.searchDisposable = searchDisposable } searchDisposable.set((searchMessages(account: self.account, location: searchState.location, query: searchState.query, lowerBound: loadMoreIndex, limit: limit) |> map { ($0.0, $0.2) } |> delay(0.2, queue: Queue.mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] results, totalCount in guard let strongSelf = self else { return } let complete = results.count != 0 strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search, let previousResultsState = data.resultsState { let previousSet = Set(previousResultsState.messageIndices) let messageIndices = results.map({ MessageIndex($0) }).sorted() var mergedIndices = messageIndices.filter({ !previousSet.contains($0) }) mergedIndices.append(contentsOf: previousResultsState.messageIndices) return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: mergedIndices, currentId: previousResultsState.currentId, totalCount: totalCount, complete: complete))) } else { return current } }) }, completed: { [weak self] in if let strongSelf = self { strongSelf.searching.set(false) } })) } else { self.searching.set(false) self.searchDisposable?.set(nil) } } } else { self.searching.set(false) self.searchDisposable?.set(nil) if let data = interfaceState.search { return interfaceState.updatedSearch(data.withUpdatedResultsState(nil)) } } } return nil } public func navigateToMessage(messageLocation: NavigateToMessageLocation, animated: Bool, completion: (() -> Void)? = nil) { self.navigateToMessage(from: nil, to: messageLocation, rememberInStack: false, animated: animated, completion: completion) } private func navigateToMessage(from fromId: MessageId?, to messageLocation: NavigateToMessageLocation, scrollPosition: ListViewScrollPosition = .center(.bottom), rememberInStack: Bool = true, animated: Bool = true, completion: (() -> Void)? = nil) { if self.isNodeLoaded { var fromIndex: MessageIndex? if let fromId = fromId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(fromId) { fromIndex = MessageIndex(message) } else { if let message = self.chatDisplayNode.historyNode.anchorMessageInCurrentHistoryView() { fromIndex = MessageIndex(message) } } if case let .peer(peerId) = self.chatLocation, messageLocation.messageId.peerId != peerId { if let navigationController = self.navigationController as? NavigationController { navigateToChatController(navigationController: navigationController, account: self.account, chatLocation: .peer(messageLocation.messageId.peerId), messageId: messageLocation.messageId) } } else if case let .peer(peerId) = self.chatLocation, messageLocation.messageId.peerId == peerId { if let fromIndex = fromIndex { if let _ = fromId, rememberInStack { self.historyNavigationStack.add(fromIndex) } if let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageLocation.messageId) { self.loadingMessage.set(false) self.messageIndexDisposable.set(nil) self.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: MessageIndex(message), animated: animated, scrollPosition: scrollPosition) completion?() } else { self.loadingMessage.set(true) let searchLocation: ChatHistoryInitialSearchLocation switch messageLocation { case let .id(id): searchLocation = .id(id) case let .index(index): searchLocation = .index(index) } let historyView = chatHistoryViewForLocation(.InitialSearch(location: searchLocation, count: 50), account: self.account, chatLocation: self.chatLocation, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView |> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in switch historyView { case .Loading: return .single((nil, true)) case let .HistoryView(view, _, _, _, _): for entry in view.entries { if case let .MessageEntry(message, _, _, _) = entry { if message.id == messageLocation.messageId { return .single((MessageIndex(message), false)) } } } if case let .index(index) = searchLocation { return .single((index, false)) } return .single((nil, false)) } } |> take(until: { index in return SignalTakeAction(passthrough: true, complete: !index.1) }) var cancelImpl: (() -> Void)? let presentationData = self.presentationData let progressSignal = Signal { [weak self] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, 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 = MetaDisposable() var progressStarted = false self.messageIndexDisposable.set((signal |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } |> deliverOnMainQueue).start(next: { [weak self] index in if let strongSelf = self, let index = index.0 { strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index, animated: animated, scrollPosition: scrollPosition) completion?() } else if index.1 { if !progressStarted { progressStarted = true progressDisposable.set(progressSignal.start()) } } }, completed: { [weak self] in if let strongSelf = self { strongSelf.loadingMessage.set(false) } })) cancelImpl = { [weak self] in if let strongSelf = self { strongSelf.loadingMessage.set(false) strongSelf.messageIndexDisposable.set(nil) } } } } else { completion?() } } else { if let fromIndex = fromIndex { if let _ = fromId, rememberInStack { self.historyNavigationStack.add(fromIndex) } self.loadingMessage.set(true) let searchLocation: ChatHistoryInitialSearchLocation switch messageLocation { case let .id(id): searchLocation = .id(id) case let .index(index): searchLocation = .index(index) } let historyView = chatHistoryViewForLocation(.InitialSearch(location: searchLocation, count: 50), account: self.account, chatLocation: self.chatLocation, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView |> mapToSignal { historyView -> Signal in switch historyView { case .Loading: return .complete() case let .HistoryView(view, _, _, _, _): for entry in view.entries { if case let .MessageEntry(message, _, _, _) = entry { if message.id == messageLocation.messageId { return .single(MessageIndex(message)) } } } return .single(nil) } } |> take(1) self.messageIndexDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] index in if let strongSelf = self { if let index = index { strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index, animated: animated, scrollPosition: scrollPosition) completion?() } else { (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(messageLocation.messageId.peerId), messageId: messageLocation.messageId)) completion?() } } }, completed: { [weak self] in if let strongSelf = self { strongSelf.loadingMessage.set(false) } })) } } } else { completion?() } } private func forwardMessages(messageIds: [MessageId]) { let controller = PeerSelectionController(account: self.account, filter: .onlyWriteable) controller.peerSelected = { [weak self, weak controller] peerId in guard let strongSelf = self, let strongController = controller else { return } if case .peer(peerId) = strongSelf.chatLocation { strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(messageIds).withoutSelectionState() }) }) strongController.dismiss() } else if peerId == strongSelf.account.peerId { let _ = (enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in return .forward(source: id, grouping: .auto) }) |> deliverOnMainQueue).start(next: { messageIds in if let strongSelf = self { let signals: [Signal] = messageIds.compactMap({ id -> Signal? in guard let id = id else { return nil } return strongSelf.account.pendingMessageManager.pendingMessageStatus(id) |> mapToSignal { status -> Signal in if status != nil { return .never() } else { return .single(true) } } |> take(1) }) if strongSelf.shareStatusDisposable == nil { strongSelf.shareStatusDisposable = MetaDisposable() } strongSelf.shareStatusDisposable?.set((combineLatest(signals) |> deliverOnMainQueue).start(completed: { guard let strongSelf = self else { return } strongSelf.present(OverlayStatusController(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, type: .success), in: .window(.root)) })) } }) strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) strongController.dismiss() } else { let _ = (strongSelf.account.postbox.transaction({ transaction -> Void in transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedForwardMessageIds(messageIds) } else { return ChatInterfaceState().withUpdatedForwardMessageIds(messageIds) } }) }) |> deliverOnMainQueue).start(completed: { if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) let ready = ValuePromise() strongSelf.controllerNavigationDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in if let strongController = controller { strongController.dismiss() } })) (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready) } }) } } self.chatDisplayNode.dismissInput() self.present(controller, in: .window(.root)) } private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: Message?) { if case let .peer(currentPeerId) = self.chatLocation, peerId == currentPeerId { switch navigation { case .info: self.navigationButtonAction(.openChatInfo) case let .chat(textInputState, _): if let textInputState = textInputState { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { return ($0.updatedInterfaceState { return $0.withUpdatedComposeInputState(textInputState) }).updatedInputMode({ _ in return .text }) }) } case let .withBotStartPayload(botStart): self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedBotStartPayload(botStart.payload) }) default: break } } else { if let peerId = peerId { switch self.chatLocation { case .peer: switch navigation { case .info: let peerSignal: Signal if let fromMessage = fromMessage { peerSignal = loadedPeerFromMessage(account: self.account, peerId: peerId, messageId: fromMessage.id) } else { peerSignal = self.account.postbox.loadedPeerWithId(peerId) |> map(Optional.init) } self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, let peer = peer { if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } } })) case let .chat(textInputState, messageId): if let textInputState = textInputState { let _ = (self.account.postbox.transaction({ transaction -> Void in transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedComposeInputState(textInputState) } else { return ChatInterfaceState().withUpdatedComposeInputState(textInputState) } }) }) |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId), messageId: nil)) } }) } else { (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, chatLocation: .peer(peerId), messageId: nil)) } case let .withBotStartPayload(botStart): (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, chatLocation: .peer(peerId), messageId: nil, botStart: botStart)) default: break } case .group: (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, chatLocation: .peer(peerId), messageId: fromMessage?.id, botStart: nil)) } } else { switch navigation { case .info: break case let .chat(textInputState, _): if let textInputState = textInputState { let controller = PeerSelectionController(account: self.account) controller.peerSelected = { [weak self, weak controller] peerId in if let strongSelf = self, let strongController = controller { if case let .peer(currentPeerId) = strongSelf.chatLocation, peerId == currentPeerId { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { return ($0.updatedInterfaceState { return $0.withUpdatedComposeInputState(textInputState) }).updatedInputMode({ _ in return .text }) }) strongController.dismiss() } else { let _ = (strongSelf.account.postbox.transaction({ transaction -> Void in transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedComposeInputState(textInputState) } else { return ChatInterfaceState().withUpdatedComposeInputState(textInputState) } }) }) |> deliverOnMainQueue).start(completed: { if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) let ready = ValuePromise() strongSelf.controllerNavigationDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in if let strongController = controller { strongController.dismiss() } })) (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready) } }) } } } self.chatDisplayNode.dismissInput() self.present(controller, in: .window(.root)) } default: break } } } } private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default) { let disposable: MetaDisposable if let resolvePeerByNameDisposable = self.resolvePeerByNameDisposable { disposable = resolvePeerByNameDisposable } else { disposable = MetaDisposable() self.resolvePeerByNameDisposable = disposable } var resolveSignal = resolvePeerByName(account: self.account, name: name, ageLimit: 10) var cancelImpl: (() -> Void)? let presentationData = self.presentationData let progressSignal = Signal { [weak self] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, 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 |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in if let strongSelf = self { strongSelf.openResolved(.peer(peerId, navigation)) } })) } private func unblockPeer() { guard case let .peer(peerId) = self.chatLocation else { return } let unblockingPeer = self.unblockingPeer unblockingPeer.set(true) var restartBot = false if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { restartBot = true } self.editMessageDisposable.set((requestUpdatePeerIsBlocked(account: self.account, peerId: peerId, isBlocked: false) |> afterDisposed({ [weak self] in Queue.mainQueue().async { unblockingPeer.set(false) if let strongSelf = self, restartBot { let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() } } })).start()) } private func reportPeer() { if let peer = self.presentationInterfaceState.renderedPeer?.peer { let title: String var infoString: String? if let _ = peer as? TelegramGroup { title = self.presentationData.strings.Conversation_ReportSpam } else if let _ = peer as? TelegramChannel { title = self.presentationData.strings.Conversation_ReportSpam } else { title = self.presentationData.strings.Conversation_ReportSpam infoString = self.presentationData.strings.Conversation_ReportSpamConfirmation } let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) var items: [ActionSheetItem] = [] if let infoString = infoString { items.append(ActionSheetTextItem(title: infoString)) } items.append(ActionSheetButtonItem(title: title, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.deleteChat(reportChatSpam: true) } })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) self.chatDisplayNode.dismissInput() self.present(actionSheet, in: .window(.root)) } } private func addPeerContact() { if let peer = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty, let contactData = DeviceContactExtendedData(peer: peer) { self.present(addContactOptionsController(account: self.account, peer: peer, contactData: contactData), in: .window(.root)) } } private func dismissReportPeer() { guard case let .peer(peerId) = self.chatLocation else { return } self.editMessageDisposable.set((TelegramCore.dismissReportPeer(account: self.account, peerId: peerId) |> afterDisposed({ Queue.mainQueue().async { } })).start()) } private func deleteChat(reportChatSpam: Bool) { guard case let .peer(peerId) = self.chatLocation else { return } self.chatDisplayNode.historyNode.disconnect() let _ = removePeerChat(postbox: self.account.postbox, peerId: peerId, reportChatSpam: reportChatSpam).start() (self.navigationController as? NavigationController)?.popToRoot(animated: true) let _ = requestUpdatePeerIsBlocked(account: self.account, peerId: peerId, isBlocked: true).start() } private func startBot(_ payload: String?) { guard case let .peer(peerId) = self.chatLocation else { return } let startingBot = self.startingBot startingBot.set(true) self.editMessageDisposable.set((requestStartBot(account: self.account, botPeerId: peerId, payload: payload) |> deliverOnMainQueue |> afterDisposed({ startingBot.set(false) })).start(completed: { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedBotStartPayload(nil) }) } })) } private func openResolved(_ result: ResolvedUrl) { openResolvedUrl(result, account: self.account, context: .chat, navigationController: self.navigationController as? NavigationController, openPeer: { [weak self] peerId, navigation in guard let strongSelf = self else { return } switch navigation { case let .chat(_, messageId): if case .peer(peerId) = strongSelf.chatLocation { if let messageId = messageId { strongSelf.navigateToMessage(from: nil, to: .id(messageId)) } } else if let navigationController = strongSelf.navigationController as? NavigationController { navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), messageId: messageId, keepStack: .always) } case .info: strongSelf.navigationActionDisposable.set((strongSelf.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, peer.restrictionText == nil { if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } } })) case let .withBotStartPayload(startPayload): if case .peer(peerId) = strongSelf.chatLocation { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedBotStartPayload(startPayload.payload) }) } else if let navigationController = strongSelf.navigationController as? NavigationController { navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), botStart: startPayload) } default: break } }, sendFile: { [weak self] f in self?.interfaceInteraction?.sendSticker(f) }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, dismissInput: { [weak self] in self?.chatDisplayNode.dismissInput() }) } private func openUrl(_ url: String, concealed: Bool) { let openImpl: () -> Void = { [weak self] in guard let strongSelf = self else { return } let disposable: MetaDisposable if let current = strongSelf.resolveUrlDisposable { disposable = current } else { disposable = MetaDisposable() strongSelf.resolveUrlDisposable = disposable } var cancelImpl: (() -> Void)? let presentationData = strongSelf.presentationData let progressSignal = Signal { subscriber in let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, 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() cancelImpl = { [weak self] in self?.resolveUrlDisposable?.set(nil) } disposable.set((resolveUrl(account: strongSelf.account, url: url) |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { strongSelf.openResolved(result) } })) } var parsedUrlValue: URL? if let parsed = URL(string: url) { parsedUrlValue = parsed } else if let encoded = (url as NSString).addingPercentEscapes(using: String.Encoding.utf8.rawValue), let parsed = URL(string: encoded) { parsedUrlValue = parsed } if concealed, let parsedUrlValue = parsedUrlValue, (parsedUrlValue.scheme == "http" || parsedUrlValue.scheme == "https"), !isConcealedUrlWhitelisted(parsedUrlValue) { var displayUrl = url let maxLength = 180 if displayUrl.count > maxLength { displayUrl = String(displayUrl[.. (UIViewController, CGRect)? { guard let buttonView = (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.view else { return nil } if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer, peer.smallProfileImage != nil { let galleryController = AvatarGalleryController(account: self.account, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in }, synchronousLoad: true) galleryController.setHintWillBePresentedInPreviewingContext(true) galleryController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) return (galleryController, buttonView.convert(buttonView.bounds, to: sourceView)) } return nil } func previewingController(from sourceView: UIView, for location: CGPoint) -> (UIViewController, CGRect)? { let historyPoint = sourceView.convert(location, to: self.chatDisplayNode.historyNode.view) var result: (Message, ChatMessagePeekPreviewContent)? self.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if itemNode.frame.contains(historyPoint) { if let value = itemNode.peekPreviewContent(at: self.chatDisplayNode.historyNode.view.convert(historyPoint, to: itemNode.view)) { result = value } } } } if let (message, content) = result { switch content { case let .media(media): var selectedTransitionNode: (ASDisplayNode, () -> UIView?)? self.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: message.id, media: media) { selectedTransitionNode = result } } } if let selectedTransitionNode = selectedTransitionNode { if let previewData = chatMessagePreviewControllerData(account: self.account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: self.navigationController as? NavigationController) { switch previewData { case let .gallery(gallery): gallery.setHintWillBePresentedInPreviewingContext(true) let rect = selectedTransitionNode.0.view.convert(selectedTransitionNode.0.bounds, to: sourceView) let sourceRect = rect.insetBy(dx: -2.0, dy: -2.0) gallery.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) return (gallery, sourceRect) case let .instantPage(gallery, centralIndex, galleryMedia): break } } } case let .url(node, rect, string): let targetRect = node.view.convert(rect, to: sourceView) let sourceRect = CGRect(origin: CGPoint(x: floor(targetRect.midX), y: floor(targetRect.midY)), size: CGSize(width: 1.0, height: 1.0)) if let parsedUrl = URL(string: string) { if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { if #available(iOSApplicationExtension 9.0, *) { let controller = SFSafariViewController(url: parsedUrl) if #available(iOSApplicationExtension 10.0, *) { controller.preferredBarTintColor = self.presentationData.theme.rootController.navigationBar.backgroundColor controller.preferredControlTintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor } return (controller, sourceRect) } } } } } return nil } func previewingCommit(_ viewControllerToCommit: UIViewController) { if let gallery = viewControllerToCommit as? AvatarGalleryController { self.chatDisplayNode.dismissInput() gallery.setHintWillBePresentedInPreviewingContext(false) self.present(gallery, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(animated: false, transitionArguments: { _ in return nil })) } else if let gallery = viewControllerToCommit as? GalleryController { self.chatDisplayNode.dismissInput() gallery.setHintWillBePresentedInPreviewingContext(false) self.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(animated: false, transitionArguments: { [weak self] messageId, media in if let strongSelf = self { var selectedTransitionNode: (ASDisplayNode, () -> UIView?)? strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: messageId, media: media) { selectedTransitionNode = result } } } if let selectedTransitionNode = selectedTransitionNode { return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: { view in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view) } }) } } return nil })) } else if let gallery = viewControllerToCommit as? InstantPageGalleryController { } if #available(iOSApplicationExtension 9.0, *) { if let safariController = viewControllerToCommit as? SFSafariViewController { if let window = self.navigationController?.view.window { window.rootViewController?.present(safariController, animated: true) } } } } @available(iOSApplicationExtension 9.0, *) override public var previewActionItems: [UIPreviewActionItem] { struct PreviewActionsData { let notificationSettings: PeerNotificationSettings? let peer: Peer? } let chatLocation = self.chatLocation let data = Atomic(value: nil) let semaphore = DispatchSemaphore(value: 0) let _ = self.account.postbox.transaction({ transaction -> Void in switch chatLocation { case let .peer(peerId): let _ = data.swap(PreviewActionsData(notificationSettings: transaction.getPeerNotificationSettings(peerId), peer: transaction.getPeer(peerId))) case .group: let _ = data.swap(PreviewActionsData(notificationSettings: nil, peer: nil)) } semaphore.signal() }).start() semaphore.wait() return data.with { [weak self] data -> [UIPreviewActionItem] in var items: [UIPreviewActionItem] = [] if let data = data, let strongSelf = self { let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } switch strongSelf.peekActions { case .standard: if let peer = data.peer { if let _ = data.peer as? TelegramUser { items.append(UIPreviewAction(title: "👍", style: .default, handler: { _, _ in if let strongSelf = self { let _ = enqueueMessages(account: strongSelf.account, peerId: peer.id, messages: strongSelf.transformEnqueueMessages([.message(text: "👍", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)])).start() } })) } if let notificationSettings = data.notificationSettings as? TelegramPeerNotificationSettings { if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { items.append(UIPreviewAction(title: presentationData.strings.Conversation_Unmute, style: .default, handler: { _, _ in if let strongSelf = self { let _ = togglePeerMuted(account: strongSelf.account, peerId: peer.id).start() } })) } else { let muteInterval: Int32 if let _ = data.peer as? TelegramChannel { muteInterval = Int32.max } else { muteInterval = 1 * 60 * 60 } let title: String if muteInterval == Int32.max { title = presentationData.strings.Conversation_Mute } else { title = muteForIntervalString(strings: presentationData.strings, value: muteInterval) } items.append(UIPreviewAction(title: title, style: .default, handler: { _, _ in if let strongSelf = self { let _ = updatePeerMuteSetting(account: strongSelf.account, peerId: peer.id, muteInterval: muteInterval).start() } })) } } } case let .remove(action): items.append(UIPreviewAction(title: presentationData.strings.Common_Delete, style: .destructive, handler: { _, _ in action() })) } } return items } } private func debugStreamSingleVideo(_ id: MessageId) { let gallery = GalleryController(account: self.account, source: .peerMessagesAtId(id), streamSingleVideo: true, replaceRootController: { [weak self] controller, ready in if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.replaceTopController(controller, animated: false, ready: ready) } }, baseNavigationController: self.navigationController as? NavigationController) self.chatDisplayNode.dismissInput() self.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in if let strongSelf = self { var transitionNode: (ASDisplayNode, () -> UIView?)? strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: messageId, media: media) { transitionNode = result } } } if let transitionNode = transitionNode { return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view) } }) } } return nil })) } private func presentBanMessageOptions(author: Peer, messageIds: Set, options: ChatAvailableMessageActionOptions) { if case let .peer(peerId) = self.chatLocation { self.navigationActionDisposable.set((fetchChannelParticipant(account: self.account, peerId: peerId, participantId: author.id) |> deliverOnMainQueue).start(next: { [weak self] participant in if let strongSelf = self { var canBan = true if let participant = participant { switch participant { case .creator: canBan = false case let .member(_, _, adminInfo, _): if let adminInfo = adminInfo, !adminInfo.rights.flags.isEmpty { canBan = false } } } let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) var items: [ActionSheetItem] = [] var actions = Set([0]) let toggleCheck: (Int, Int) -> Void = { [weak actionSheet] category, itemIndex in if actions.contains(category) { actions.remove(category) } else { actions.insert(category) } actionSheet?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in if let item = item as? ActionSheetCheckboxItem { return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) } return item }) } var itemIndex = 0 var categories: [Int] = [0] if canBan { categories.append(1) } categories.append(contentsOf: [2, 3]) for categoryId in categories as [Int] { var title = "" if categoryId == 0 { title = strongSelf.presentationData.strings.Conversation_Moderate_Delete } else if categoryId == 1 { title = strongSelf.presentationData.strings.Conversation_Moderate_Ban } else if categoryId == 2 { title = strongSelf.presentationData.strings.Conversation_Moderate_Report } else if categoryId == 3 { title = strongSelf.presentationData.strings.Conversation_Moderate_DeleteAllMessages(author.displayTitle).0 } let index = itemIndex items.append(ActionSheetCheckboxItem(title: title, label: "", value: actions.contains(categoryId), action: { value in toggleCheck(categoryId, index) })) itemIndex += 1 } items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Done, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) if actions.contains(3) { let _ = strongSelf.account.postbox.transaction({ transaction -> Void in transaction.removeAllMessagesWithAuthor(peerId, authorId: author.id) }).start() let _ = clearAuthorHistory(account: strongSelf.account, peerId: peerId, memberId: author.id).start() } else if actions.contains(0) { let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() } if actions.contains(1) { let _ = removePeerMember(account: strongSelf.account, peerId: peerId, memberId: author.id).start() } } })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) } })) } } private func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions) { let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) var items: [ActionSheetItem] = [] var personalPeerName: String? var isChannel = false if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser { personalPeerName = user.compactDisplayTitle } else if let peer = self.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let associatedPeerId = peer.associatedPeerId, let user = self.presentationInterfaceState.renderedPeer?.peers[associatedPeerId] as? TelegramUser { personalPeerName = user.compactDisplayTitle } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info { isChannel = true } if options.contains(.deleteGlobally) { let globalTitle: String if isChannel { globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone } else if let personalPeerName = personalPeerName { globalTitle = self.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0 } else { globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone } items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() } })) } if options.contains(.deleteLocally) { var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe if case .peer(self.account.peerId) = self.chatLocation { if messageIds.count == 1 { localOptionText = self.presentationData.strings.Conversation_Moderate_Delete } else { localOptionText = self.presentationData.strings.Conversation_DeleteManyMessages } } items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forLocalPeer).start() } })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) self.chatDisplayNode.dismissInput() self.present(actionSheet, in: .window(.root)) } @available(iOSApplicationExtension 11.0, *) public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String]) } @available(iOSApplicationExtension 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, *) public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in guard let strongSelf = self else { return } let images = imageItems as! [UIImage] strongSelf.chatDisplayNode.updateDropInteraction(isActive: false) strongSelf.displayPasteMenu(images) } } @available(iOSApplicationExtension 11.0, *) public func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) { self.chatDisplayNode.updateDropInteraction(isActive: false) } @available(iOSApplicationExtension 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) } private func displayMediaRecordingTip() { let rect: CGRect? = self.chatDisplayNode.frameForInputActionButton() let updatedMode: ChatTextInputMediaRecordingButtonMode = self.presentationInterfaceState.interfaceState.mediaRecordingMode let text: String if updatedMode == .audio { text = self.presentationData.strings.Conversation_HoldForAudio } else { text = self.presentationData.strings.Conversation_HoldForVideo } if let tooltipController = self.mediaRecordingModeTooltipController { tooltipController.text = text } else if let rect = rect { let tooltipController = TooltipController(text: text) 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 })) } } private func commitPurposefulAction() { self.purposefulAction?() } public var keyShortcuts: [KeyShortcut] { let strings = self.presentationData.strings var inputShortcuts: [KeyShortcut] if self.chatDisplayNode.isInputViewFocused { inputShortcuts = [KeyShortcut(title: strings.KeyCommand_SendMessage, input: "\r", action: { })] } else { inputShortcuts = [KeyShortcut(title: strings.KeyCommand_FocusOnInputField, input: "\r", action: { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in return state.updatedInterfaceState { interfaceState in return interfaceState.withUpdatedEffectiveInputState(interfaceState.effectiveInputState) }.updatedInputMode({ _ in ChatInputMode.text }) }) } }), KeyShortcut(input: "/", modifiers: [], action: { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in if state.interfaceState.effectiveInputState.inputText.length == 0 { return state.updatedInterfaceState { interfaceState in let effectiveInputState = ChatTextInputState(inputText: NSAttributedString(string: "/")) return interfaceState.withUpdatedEffectiveInputState(effectiveInputState) }.updatedInputMode({ _ in ChatInputMode.text }) } else { return state } }) } }), KeyShortcut(input: "2", modifiers: [.shift], action: { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in if state.interfaceState.effectiveInputState.inputText.length == 0 { return state.updatedInterfaceState { interfaceState in let effectiveInputState = ChatTextInputState(inputText: NSAttributedString(string: "@")) return interfaceState.withUpdatedEffectiveInputState(effectiveInputState) }.updatedInputMode({ _ in ChatInputMode.text }) } else { return state } }) } }), KeyShortcut(input: "3", modifiers: [.shift], action: { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in if state.interfaceState.effectiveInputState.inputText.length == 0 { return state.updatedInterfaceState { interfaceState in let effectiveInputState = ChatTextInputState(inputText: NSAttributedString(string: "#")) return interfaceState.withUpdatedEffectiveInputState(effectiveInputState) }.updatedInputMode({ _ in ChatInputMode.text }) } else { return state } }) } }) ] if true { inputShortcuts.append(KeyShortcut(input: UIKeyInputUpArrow, action: { [weak self] in if let strongSelf = self { } })) } } let otherShortcuts: [KeyShortcut] = [ KeyShortcut(title: strings.KeyCommand_ScrollUp, input: UIKeyInputUpArrow, modifiers: [.shift], action: { [weak self] in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.scrollWithDeltaOffset(75) } }), KeyShortcut(title: strings.KeyCommand_ScrollDown, input: UIKeyInputDownArrow, modifiers: [.shift], action: { [weak self] in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.scrollWithDeltaOffset(-75) } }), KeyShortcut(title: strings.KeyCommand_ChatInfo, input: "I", modifiers: [.command, .control], action: { [weak self] in if let strongSelf = self { strongSelf.interfaceInteraction?.openPeerInfo() } }), KeyShortcut(input: "/", modifiers: [.command], action: { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in return state.updatedInterfaceState { interfaceState in return interfaceState.withUpdatedEffectiveInputState(interfaceState.effectiveInputState) }.updatedInputMode({ _ in ChatInputMode.media(mode: .other, expanded: nil) }) }) } }), KeyShortcut(input: "W", modifiers: [.command], action: { [weak self] in if let strongSelf = self { } }) ] return inputShortcuts + otherShortcuts } }