Swiftgram/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift
2023-12-12 14:05:58 +04:00

517 lines
29 KiB
Swift

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 (updatedUrlPreviewState, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: selfController.context, currentQuery: selfController.urlPreviewQueryState?.0, forPeerId: selfController.chatLocation.peerId) {
selfController.urlPreviewQueryState?.1.dispose()
var inScope = true
var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)?
let linkPreviews: Signal<Bool, NoError>
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, String)?, NoError> in
if value {
return updatedUrlPreviewSignal
} else {
return .single({ _ in return nil })
}
}
selfController.urlPreviewQueryState = (updatedUrlPreviewState, (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 (webpage, webpageUrl) = result($0.urlPreview?.webPage) {
let updatedPreview = ChatPresentationInterfaceState.UrlPreview(
url: webpageUrl,
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 (webpage, webpageUrl) = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.webPage) {
let updatedPreview = ChatPresentationInterfaceState.UrlPreview(
url: webpageUrl,
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 (updatedEditingUrlPreviewState, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, context: selfController.context, currentQuery: selfController.editingUrlPreviewQueryState?.0, forPeerId: selfController.chatLocation.peerId) {
selfController.editingUrlPreviewQueryState?.1.dispose()
var inScope = true
var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)?
selfController.editingUrlPreviewQueryState = (updatedEditingUrlPreviewState, (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 (webpage, webpageUrl) = result($0.editingUrlPreview?.webPage) {
let updatedPreview = ChatPresentationInterfaceState.UrlPreview(
url: webpageUrl,
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 (webpage, webpageUrl) = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.webPage) {
let updatedPreview = ChatPresentationInterfaceState.UrlPreview(
url: webpageUrl,
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(context: selfController.context, presentationInterfaceState: 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)
}