diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift new file mode 100644 index 0000000000..36713b438c --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -0,0 +1,516 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import AccountContext +import ChatPresentationInterfaceState +import ChatInterfaceState +import TelegramNotices +import PresentationDataUtils +import TelegramCallsUI +import AttachmentUI + +func updateChatPresentationInterfaceStateImpl( + selfController: ChatControllerImpl, + transition: ContainedViewLayoutTransition, + interactive: Bool, + saveInterfaceState: Bool, + _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, + completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void +) { + var completion = externalCompletion + var temporaryChatPresentationInterfaceState = f(selfController.presentationInterfaceState) + + if selfController.presentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup || selfController.presentationInterfaceState.keyboardButtonsMessage?.id != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.id { + if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, let keyboardMarkup = keyboardButtonsMessage.visibleButtonKeyboardMarkup { + if selfController.presentationInterfaceState.interfaceState.editMessage == nil && selfController.presentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId && temporaryChatPresentationInterfaceState.botStartPayload == nil { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ _ in + return .inputButtons(persistent: keyboardMarkup.flags.contains(.persistent)) + }) + } + + if case let .peer(peerId) = selfController.chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup { + if temporaryChatPresentationInterfaceState.interfaceState.replyMessageSubject == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ + $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( + messageId: keyboardButtonsMessage.id, + quote: nil + )).withUpdatedMessageActionsState({ value in + var value = value + value.processedSetupReplyMessageId = keyboardButtonsMessage.id + return value + }) }) + } + } + } 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.replyMessageSubject == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( + messageId: keyboardButtonsMessage.id, + quote: nil + )).withUpdatedMessageActionsState({ value in + var value = value + value.processedSetupReplyMessageId = keyboardButtonsMessage.id + return value + }) }) + } + } + + let inputTextPanelState = inputTextPanelStateForChatPresentationInterfaceState(temporaryChatPresentationInterfaceState, context: selfController.context) + var updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputTextPanelState({ _ in return inputTextPanelState }) + + let contextQueryUpdates = contextQueryResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, context: selfController.context, currentQueryStates: &selfController.contextQueryStates, requestBotLocationStatus: { [weak selfController] peerId in + guard let selfController else { + return + } + let _ = (ApplicationSpecificNotice.updateInlineBotLocationRequestState(accountManager: selfController.context.sharedContext.accountManager, peerId: peerId, timestamp: Int32(Date().timeIntervalSince1970 + 10 * 60)) + |> deliverOnMainQueue).startStandalone(next: { [weak selfController] value in + guard let selfController, value else { + return + } + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_OK, action: { [weak selfController] in + guard let selfController else { + return + } + let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: selfController.context.sharedContext.accountManager, peerId: peerId, value: 0).startStandalone() + })]), in: .window(.root)) + }) + }) + + for (kind, update) in contextQueryUpdates { + switch update { + case .remove: + if let (_, disposable) = selfController.contextQueryStates[kind] { + disposable.dispose() + selfController.contextQueryStates.removeValue(forKey: kind) + + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { _ in + return nil + }) + } + if case .contextRequest = kind { + selfController.performingInlineSearch.set(false) + } + case let .update(query, signal): + let currentQueryAndDisposable = selfController.contextQueryStates[kind] + currentQueryAndDisposable?.1.dispose() + + var inScope = true + var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? + selfController.contextQueryStates[kind] = (query, (signal + |> deliverOnMainQueue).startStrict(next: { [weak selfController] result in + guard let selfController else { + return + } + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInputQueryResult(queryKind: kind, { previousResult in + return result(previousResult) + }) + }) + } + }, error: { [weak selfController] error in + guard let selfController else { + return + } + if case .contextRequest = kind { + selfController.performingInlineSearch.set(false) + } + + switch error { + case .generic: + break + case let .inlineBotLocationRequest(peerId): + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Cancel, action: { [weak selfController] in + guard let selfController else { + return + } + let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: selfController.context.sharedContext.accountManager, peerId: peerId, value: Int32(Date().timeIntervalSince1970 + 10 * 60)).startStandalone() + }), TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_OK, action: { [weak selfController] in + guard let selfController else { + return + } + let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: selfController.context.sharedContext.accountManager, peerId: peerId, value: 0).startStandalone() + })]), in: .window(.root)) + } + }, completed: { [weak selfController] in + guard let selfController else { + return + } + if case .contextRequest = kind { + selfController.performingInlineSearch.set(false) + } + })) + inScope = false + if let inScopeResult = inScopeResult { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { previousResult in + return inScopeResult(previousResult) + }) + } else { + if case .contextRequest = kind { + selfController.performingInlineSearch.set(true) + } + } + + if case let .peer(peerId) = selfController.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { + if case .contextRequest = query { + let _ = (ApplicationSpecificNotice.getSecretChatInlineBotUsage(accountManager: selfController.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak selfController] value in + guard let selfController, !value else { + return + } + let _ = ApplicationSpecificNotice.setSecretChatInlineBotUsage(accountManager: selfController.context.sharedContext.accountManager).startStandalone() + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_SecretChatContextBotAlert, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + } + } + } + } + + var isBot = false + if let peer = updatedChatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil { + isBot = true + } else { + isBot = false + } + selfController.chatDisplayNode.historyNode.chatHasBots = updatedChatPresentationInterfaceState.hasBots || isBot + + if let (updatedSearchQuerySuggestionState, updatedSearchQuerySuggestionSignal) = searchQuerySuggestionResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, context: selfController.context, currentQuery: selfController.searchQuerySuggestionState?.0) { + selfController.searchQuerySuggestionState?.1.dispose() + var inScope = true + var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? + selfController.searchQuerySuggestionState = (updatedSearchQuerySuggestionState, (updatedSearchQuerySuggestionSignal |> deliverOnMainQueue).startStrict(next: { [weak selfController] result in + guard let selfController else { + return + } + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + selfController.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, context: selfController.context, currentQuery: selfController.urlPreviewQueryState?.0) { + selfController.urlPreviewQueryState?.1.dispose() + var inScope = true + var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? + let linkPreviews: Signal + if case let .peer(peerId) = selfController.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { + linkPreviews = interactiveChatLinkPreviewsEnabled(accountManager: selfController.context.sharedContext.accountManager, displayAlert: { [weak selfController] f in + guard let selfController else { + return + } + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_SecretLinkPreviewAlert, actions: [ + TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Yes, action: { + f.f(true) + }), TextAlertAction(type: .genericAction, title: selfController.presentationData.strings.Common_No, action: { + f.f(false) + })]), in: .window(.root)) + }) + } else { + var bannedEmbedLinks = false + if let channel = selfController.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banEmbedLinks) != nil { + bannedEmbedLinks = true + } else if let group = selfController.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banEmbedLinks) { + bannedEmbedLinks = true + } + if bannedEmbedLinks { + 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 }) + } + } + + selfController.urlPreviewQueryState = (updatedUrlPreviewUrl, (filteredPreviewSignal |> deliverOnMainQueue).startStrict(next: { [weak selfController] (result) in + guard let selfController else { + return + } + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { + if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = result($0.urlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedUrlPreviewUrl, + webPage: webpage, + positionBelowText: $0.urlPreview?.positionBelowText ?? true, + largeMedia: $0.urlPreview?.largeMedia + ) + return $0.updatedUrlPreview(updatedPreview) + } else { + return $0.updatedUrlPreview(nil) + } + }) + } + })) + inScope = false + if let inScopeResult = inScopeResult { + if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedUrlPreviewUrl, + webPage: webpage, + positionBelowText: updatedChatPresentationInterfaceState.urlPreview?.positionBelowText ?? true, + largeMedia: updatedChatPresentationInterfaceState.urlPreview?.largeMedia + ) + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(updatedPreview) + } else { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(nil) + } + } + } + + let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.content != .plaintext + let editingUrlPreviewText: NSAttributedString? = isEditingMedia ? nil : updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText + if let (updatedEditingUrlPreviewUrl, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, context: selfController.context, currentQuery: selfController.editingUrlPreviewQueryState?.0) { + selfController.editingUrlPreviewQueryState?.1.dispose() + var inScope = true + var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? + selfController.editingUrlPreviewQueryState = (updatedEditingUrlPreviewUrl, (updatedEditingUrlPreviewSignal |> deliverOnMainQueue).startStrict(next: { [weak selfController] result in + guard let selfController else { + return + } + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { + if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = result($0.editingUrlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedEditingUrlPreviewUrl, + webPage: webpage, + positionBelowText: $0.editingUrlPreview?.positionBelowText ?? true, + largeMedia: $0.editingUrlPreview?.largeMedia + ) + return $0.updatedEditingUrlPreview(updatedPreview) + } else { + return $0.updatedEditingUrlPreview(nil) + } + }) + } + })) + inScope = false + if let inScopeResult = inScopeResult { + if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.webPage) { + let updatedPreview = ChatPresentationInterfaceState.UrlPreview( + url: updatedEditingUrlPreviewUrl, + webPage: webpage, + positionBelowText: updatedChatPresentationInterfaceState.editingUrlPreview?.positionBelowText ?? true, + largeMedia: updatedChatPresentationInterfaceState.editingUrlPreview?.largeMedia + ) + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(updatedPreview) + } else { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(nil) + } + } + } + + if let replyMessageId = updatedChatPresentationInterfaceState.interfaceState.replyMessageSubject?.messageId { + if selfController.replyMessageState?.0 != replyMessageId { + selfController.replyMessageState?.1.dispose() + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedReplyMessage(nil) + let disposable = MetaDisposable() + selfController.replyMessageState = (replyMessageId, disposable) + disposable.set((selfController.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: replyMessageId)) + |> deliverOnMainQueue).start(next: { [weak selfController] message in + guard let selfController else { + return + } + if message != selfController.presentationInterfaceState.replyMessage.flatMap(EngineMessage.init) { + selfController.updateChatPresentationInterfaceState(interactive: false, { presentationInterfaceState in + return presentationInterfaceState.updatedReplyMessage(message?._asMessage()) + }) + } + })) + } + } else { + if let replyMessageState = selfController.replyMessageState { + selfController.replyMessageState = nil + replyMessageState.1.dispose() + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedReplyMessage(nil) + } + } + + if let updated = selfController.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 != selfController.recordingActivityValue { + selfController.recordingActivityValue = recordingActivityValue + selfController.recordingActivityPromise.set(recordingActivityValue) + } + + if (selfController.presentationInterfaceState.interfaceState.selectionState == nil) != (updatedChatPresentationInterfaceState.interfaceState.selectionState == nil) { + selfController.isSelectingMessagesUpdated?(updatedChatPresentationInterfaceState.interfaceState.selectionState != nil) + selfController.updateNextChannelToReadVisibility() + } + + selfController.presentationInterfaceState = updatedChatPresentationInterfaceState + + selfController.updateSlowmodeStatus() + + switch updatedChatPresentationInterfaceState.inputMode { + case .media: + break + default: + selfController.chatDisplayNode.collapseInput() + } + + if selfController.isNodeLoaded { + selfController.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, transition: transition, interactive: interactive, completion: completion) + } else { + completion(.immediate) + } + + let updatedServiceTasks = serviceTasksForChatPresentationIntefaceState(context: selfController.context, chatPresentationInterfaceState: updatedChatPresentationInterfaceState, updateState: { [weak selfController] f in + guard let selfController else { + return + } + selfController.updateChatPresentationInterfaceState(animated: false, interactive: false, f) + + //selfController.chatDisplayNode.updateChatPresentationInterfaceState(f(selfController.chatDisplayNode.chatPresentationInterfaceState), transition: transition, interactive: false, completion: { _ in }) + }) + for (id, begin) in updatedServiceTasks { + if selfController.stateServiceTasks[id] == nil { + selfController.stateServiceTasks[id] = begin() + } + } + var removedServiceTaskIds: [AnyHashable] = [] + for (id, _) in selfController.stateServiceTasks { + if updatedServiceTasks[id] == nil { + removedServiceTaskIds.append(id) + } + } + for id in removedServiceTaskIds { + selfController.stateServiceTasks.removeValue(forKey: id)?.dispose() + } + + if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, subject: selfController.subject, strings: updatedChatPresentationInterfaceState.strings, currentButton: selfController.leftNavigationButton, target: selfController, selector: #selector(selfController.leftNavigationButtonAction)) { + if selfController.leftNavigationButton != button { + var animated = transition.isAnimated + if let currentButton = selfController.leftNavigationButton?.action, currentButton == button.action { + animated = false + } + animated = false + selfController.navigationItem.setLeftBarButton(button.buttonItem, animated: animated) + selfController.leftNavigationButton = button + } + } else if let _ = selfController.leftNavigationButton { + selfController.navigationItem.setLeftBarButton(nil, animated: transition.isAnimated) + selfController.leftNavigationButton = nil + } + + if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: selfController.rightNavigationButton, target: selfController, selector: #selector(selfController.rightNavigationButtonAction), chatInfoNavigationButton: selfController.chatInfoNavigationButton, moreInfoNavigationButton: selfController.moreInfoNavigationButton) { + if selfController.rightNavigationButton != button { + var animated = transition.isAnimated + if let currentButton = selfController.rightNavigationButton?.action, currentButton == button.action { + animated = false + } + if case .replyThread = selfController.chatLocation { + animated = false + } + selfController.navigationItem.setRightBarButton(button.buttonItem, animated: animated) + selfController.rightNavigationButton = button + } + } else if let _ = selfController.rightNavigationButton { + selfController.navigationItem.setRightBarButton(nil, animated: transition.isAnimated) + selfController.rightNavigationButton = nil + } + + if let controllerInteraction = selfController.controllerInteraction { + if updatedChatPresentationInterfaceState.interfaceState.selectionState != controllerInteraction.selectionState { + controllerInteraction.selectionState = updatedChatPresentationInterfaceState.interfaceState.selectionState + let isBlackout = controllerInteraction.selectionState != nil + let previousCompletion = completion + completion = { [weak selfController] transition in + previousCompletion(transition) + + guard let selfController else { + return + } + (selfController.navigationController as? NavigationController)?.updateMasterDetailsBlackout(isBlackout ? .master : nil, transition: transition) + } + selfController.updateItemNodesSelectionStates(animated: transition.isAnimated) + } + } + + if saveInterfaceState { + selfController.saveInterfaceState(includeScrollState: false) + } + + if let navigationController = selfController.navigationController as? NavigationController, isTopmostChatController(selfController) { + var voiceChatOverlayController: VoiceChatOverlayController? + for controller in navigationController.globalOverlayControllers { + if let controller = controller as? VoiceChatOverlayController { + voiceChatOverlayController = controller + break + } + } + + if let controller = voiceChatOverlayController { + controller.updateVisibility() + } + } + + if let currentMenuWebAppController = selfController.currentMenuWebAppController, !selfController.presentationInterfaceState.showWebView { + selfController.currentMenuWebAppController = nil + if let currentMenuWebAppController = currentMenuWebAppController as? AttachmentController { + currentMenuWebAppController.ensureUnfocused = false + } + currentMenuWebAppController.dismiss(animated: true, completion: nil) + } + + selfController.presentationInterfaceStatePromise.set(selfController.presentationInterfaceState) +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 1881b8a122..1e61b6079d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -12073,477 +12073,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interactive: interactive, saveInterfaceState: saveInterfaceState, f, completion: completion) } - func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { - var completion = externalCompletion - var temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState) - - if self.presentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup || self.presentationInterfaceState.keyboardButtonsMessage?.id != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.id { - if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, let keyboardMarkup = keyboardButtonsMessage.visibleButtonKeyboardMarkup { - if self.presentationInterfaceState.interfaceState.editMessage == nil && self.presentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId && temporaryChatPresentationInterfaceState.botStartPayload == nil { - temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ _ in - return .inputButtons(persistent: keyboardMarkup.flags.contains(.persistent)) - }) - } - - if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup { - if temporaryChatPresentationInterfaceState.interfaceState.replyMessageSubject == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { - temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ - $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( - messageId: keyboardButtonsMessage.id, - quote: nil - )).withUpdatedMessageActionsState({ value in - var value = value - value.processedSetupReplyMessageId = keyboardButtonsMessage.id - return value - }) }) - } - } - } 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.replyMessageSubject == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { - temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( - messageId: keyboardButtonsMessage.id, - quote: nil - )).withUpdatedMessageActionsState({ value in - var value = value - value.processedSetupReplyMessageId = keyboardButtonsMessage.id - return value - }) }) - } - } - - let inputTextPanelState = inputTextPanelStateForChatPresentationInterfaceState(temporaryChatPresentationInterfaceState, context: self.context) - var updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputTextPanelState({ _ in return inputTextPanelState }) - - let contextQueryUpdates = contextQueryResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, context: self.context, currentQueryStates: &self.contextQueryStates, requestBotLocationStatus: { [weak self] peerId in - guard let strongSelf = self else { - return - } - let _ = (ApplicationSpecificNotice.updateInlineBotLocationRequestState(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, timestamp: Int32(Date().timeIntervalSince1970 + 10 * 60)) - |> deliverOnMainQueue).startStandalone(next: { value in - guard let strongSelf = self, value else { - return - } - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).startStandalone() - })]), in: .window(.root)) - }) - }) - - 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 - }) - } - if case .contextRequest = kind { - self.performingInlineSearch.set(false) - } - 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).startStrict(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) - }) - }) - } - } - }, error: { [weak self] error in - if let strongSelf = self { - if case .contextRequest = kind { - strongSelf.performingInlineSearch.set(false) - } - - switch error { - case .generic: - break - case let .inlineBotLocationRequest(peerId): - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: Int32(Date().timeIntervalSince1970 + 10 * 60)).startStandalone() - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).startStandalone() - })]), in: .window(.root)) - } - } - }, completed: { [weak self] in - if let strongSelf = self { - if case .contextRequest = kind { - strongSelf.performingInlineSearch.set(false) - } - } - })) - inScope = false - if let inScopeResult = inScopeResult { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { previousResult in - return inScopeResult(previousResult) - }) - } else { - if case .contextRequest = kind { - self.performingInlineSearch.set(true) - } - } - - if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { - if case .contextRequest = query { - let _ = (ApplicationSpecificNotice.getSecretChatInlineBotUsage(accountManager: self.context.sharedContext.accountManager) - |> deliverOnMainQueue).startStandalone(next: { [weak self] value in - if let strongSelf = self, !value { - let _ = ApplicationSpecificNotice.setSecretChatInlineBotUsage(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone() - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Conversation_SecretChatContextBotAlert, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - } - }) - } - } - } - } - - var isBot = false - if let peer = updatedChatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil { - isBot = true - } else { - isBot = false - } - self.chatDisplayNode.historyNode.chatHasBots = updatedChatPresentationInterfaceState.hasBots || isBot - - if let (updatedSearchQuerySuggestionState, updatedSearchQuerySuggestionSignal) = searchQuerySuggestionResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, context: context, currentQuery: self.searchQuerySuggestionState?.0) { - self.searchQuerySuggestionState?.1.dispose() - var inScope = true - var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? - self.searchQuerySuggestionState = (updatedSearchQuerySuggestionState, (updatedSearchQuerySuggestionSignal |> deliverOnMainQueue).startStrict(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, context: self.context, 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(accountManager: self.context.sharedContext.accountManager, displayAlert: { [weak self] f in - if let strongSelf = self { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, 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 { - var bannedEmbedLinks = false - if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banEmbedLinks) != nil { - bannedEmbedLinks = true - } else if let group = self.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banEmbedLinks) { - bannedEmbedLinks = true - } - if bannedEmbedLinks { - 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).startStrict(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?.webPage) { - let updatedPreview = ChatPresentationInterfaceState.UrlPreview( - url: updatedUrlPreviewUrl, - webPage: webpage, - positionBelowText: $0.urlPreview?.positionBelowText ?? true, - largeMedia: $0.urlPreview?.largeMedia - ) - return $0.updatedUrlPreview(updatedPreview) - } else { - return $0.updatedUrlPreview(nil) - } - }) - } - } - })) - inScope = false - if let inScopeResult = inScopeResult { - if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.webPage) { - let updatedPreview = ChatPresentationInterfaceState.UrlPreview( - url: updatedUrlPreviewUrl, - webPage: webpage, - positionBelowText: updatedChatPresentationInterfaceState.urlPreview?.positionBelowText ?? true, - largeMedia: updatedChatPresentationInterfaceState.urlPreview?.largeMedia - ) - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(updatedPreview) - } else { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(nil) - } - } - } - - let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.content != .plaintext - let editingUrlPreviewText: NSAttributedString? = isEditingMedia ? nil : updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText - if let (updatedEditingUrlPreviewUrl, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, context: self.context, currentQuery: self.editingUrlPreviewQueryState?.0) { - self.editingUrlPreviewQueryState?.1.dispose() - var inScope = true - var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? - self.editingUrlPreviewQueryState = (updatedEditingUrlPreviewUrl, (updatedEditingUrlPreviewSignal |> deliverOnMainQueue).startStrict(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?.webPage) { - let updatedPreview = ChatPresentationInterfaceState.UrlPreview( - url: updatedEditingUrlPreviewUrl, - webPage: webpage, - positionBelowText: $0.editingUrlPreview?.positionBelowText ?? true, - largeMedia: $0.editingUrlPreview?.largeMedia - ) - return $0.updatedEditingUrlPreview(updatedPreview) - } else { - return $0.updatedEditingUrlPreview(nil) - } - }) - } - } - })) - inScope = false - if let inScopeResult = inScopeResult { - if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.webPage) { - let updatedPreview = ChatPresentationInterfaceState.UrlPreview( - url: updatedEditingUrlPreviewUrl, - webPage: webpage, - positionBelowText: updatedChatPresentationInterfaceState.editingUrlPreview?.positionBelowText ?? true, - largeMedia: updatedChatPresentationInterfaceState.editingUrlPreview?.largeMedia - ) - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(updatedPreview) - } else { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(nil) - } - } - } - - if let replyMessageId = updatedChatPresentationInterfaceState.interfaceState.replyMessageSubject?.messageId { - if self.replyMessageState?.0 != replyMessageId { - self.replyMessageState?.1.dispose() - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedReplyMessage(nil) - let disposable = MetaDisposable() - self.replyMessageState = (replyMessageId, disposable) - disposable.set((self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: replyMessageId)) - |> deliverOnMainQueue).start(next: { [weak self] message in - guard let self else { - return - } - if message != self.presentationInterfaceState.replyMessage.flatMap(EngineMessage.init) { - self.updateChatPresentationInterfaceState(interactive: false, { presentationInterfaceState in - return presentationInterfaceState.updatedReplyMessage(message?._asMessage()) - }) - } - })) - } - } else { - if let replyMessageState = self.replyMessageState { - self.replyMessageState = nil - replyMessageState.1.dispose() - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedReplyMessage(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) - } - - if (self.presentationInterfaceState.interfaceState.selectionState == nil) != (updatedChatPresentationInterfaceState.interfaceState.selectionState == nil) { - self.isSelectingMessagesUpdated?(updatedChatPresentationInterfaceState.interfaceState.selectionState != nil) - self.updateNextChannelToReadVisibility() - } - - self.presentationInterfaceState = updatedChatPresentationInterfaceState - - self.updateSlowmodeStatus() - - switch updatedChatPresentationInterfaceState.inputMode { - case .media: - break - default: - self.chatDisplayNode.collapseInput() - } - - if self.isNodeLoaded { - self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, transition: transition, interactive: interactive, completion: completion) - } else { - completion(.immediate) - } - - let updatedServiceTasks = serviceTasksForChatPresentationIntefaceState(context: self.context, chatPresentationInterfaceState: updatedChatPresentationInterfaceState, updateState: { [weak self] f in - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, f) - - //strongSelf.chatDisplayNode.updateChatPresentationInterfaceState(f(strongSelf.chatDisplayNode.chatPresentationInterfaceState), transition: transition, interactive: false, completion: { _ in }) - }) - for (id, begin) in updatedServiceTasks { - if self.stateServiceTasks[id] == nil { - self.stateServiceTasks[id] = begin() - } - } - var removedServiceTaskIds: [AnyHashable] = [] - for (id, _) in self.stateServiceTasks { - if updatedServiceTasks[id] == nil { - removedServiceTaskIds.append(id) - } - } - for id in removedServiceTaskIds { - self.stateServiceTasks.removeValue(forKey: id)?.dispose() - } - - if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, subject: self.subject, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { - if self.leftNavigationButton != button { - var animated = transition.isAnimated - if let currentButton = self.leftNavigationButton?.action, currentButton == button.action { - animated = false - } - animated = false - self.navigationItem.setLeftBarButton(button.buttonItem, animated: animated) - 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, moreInfoNavigationButton: self.moreInfoNavigationButton) { - if self.rightNavigationButton != button { - var animated = transition.isAnimated - if let currentButton = self.rightNavigationButton?.action, currentButton == button.action { - animated = false - } - if case .replyThread = self.chatLocation { - animated = false - } - self.navigationItem.setRightBarButton(button.buttonItem, animated: animated) - 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 - let isBlackout = controllerInteraction.selectionState != nil - let previousCompletion = completion - completion = { [weak self] transition in - previousCompletion(transition) - (self?.navigationController as? NavigationController)?.updateMasterDetailsBlackout(isBlackout ? .master : nil, transition: transition) - } - self.updateItemNodesSelectionStates(animated: transition.isAnimated) - } - } - - if saveInterfaceState { - self.saveInterfaceState(includeScrollState: false) - } - - if let navigationController = self.navigationController as? NavigationController, isTopmostChatController(self) { - var voiceChatOverlayController: VoiceChatOverlayController? - for controller in navigationController.globalOverlayControllers { - if let controller = controller as? VoiceChatOverlayController { - voiceChatOverlayController = controller - break - } - } - - if let controller = voiceChatOverlayController { - controller.updateVisibility() - } - } - - if let currentMenuWebAppController = self.currentMenuWebAppController, !self.presentationInterfaceState.showWebView { - self.currentMenuWebAppController = nil - if let currentMenuWebAppController = currentMenuWebAppController as? AttachmentController { - currentMenuWebAppController.ensureUnfocused = false - } - currentMenuWebAppController.dismiss(animated: true, completion: nil) - } - - self.presentationInterfaceStatePromise.set(self.presentationInterfaceState) + func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { + updateChatPresentationInterfaceStateImpl( + selfController: self, + transition: transition, + interactive: interactive, + saveInterfaceState: saveInterfaceState, + f, + completion: completion + ) } func updateItemNodesSelectionStates(animated: Bool) {