diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift new file mode 100644 index 0000000000..014e6ac8df --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -0,0 +1,412 @@ +import Foundation +import UIKit +import Postbox +import TelegramCore +import SwiftSignalKit +import Display +import ChatPresentationInterfaceState +import AccountContext +import ChatControllerInteraction +import OverlayStatusController +import TelegramPresentationData +import PresentationDataUtils + +extension ChatControllerImpl { + func navigateToMessage( + fromId: MessageId, + id: MessageId, + params: NavigateToMessageParams + ) { + var id = id + if case let .replyThread(message) = self.chatLocation { + if let channelMessageId = message.channelMessageId, id == channelMessageId { + id = message.messageId + } + } + + let continueNavigation: () -> Void = { [weak self] in + guard let self else { + return + } + self.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId) + } + + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: id.peerId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] toPeer in + guard let self else { + return + } + + if params.quote != nil { + if let toPeer { + switch toPeer { + case let .channel(channel): + if channel.username == nil && channel.usernames.isEmpty { + switch channel.participationStatus { + case .kicked, .left: + self.controllerInteraction?.attemptedNavigationToPrivateQuote(toPeer._asPeer()) + return + case .member: + break + } + } + default: + break + } + } else { + self.controllerInteraction?.attemptedNavigationToPrivateQuote(nil) + return + } + } + + continueNavigation() + }) + } + + func navigateToMessage( + from fromId: MessageId?, + to messageLocation: NavigateToMessageLocation, + scrollPosition: ListViewScrollPosition = .center(.bottom), + rememberInStack: Bool = true, + forceInCurrentChat: Bool = false, + dropStack: Bool = false, + animated: Bool = true, + completion: (() -> Void)? = nil, + customPresentProgress: ((ViewController, Any?) -> Void)? = nil, + statusSubject: ChatLoadingMessageSubject = .generic + ) { + if !self.isNodeLoaded { + completion?() + return + } + var fromIndex: MessageIndex? + + if let fromId = fromId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(fromId) { + fromIndex = message.index + } else { + if let message = self.chatDisplayNode.historyNode.anchorMessageInCurrentHistoryView() { + fromIndex = message.index + } + } + + var isScheduledMessages = false + var isPinnedMessages = false + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } else if case .pinnedMessages = self.presentationInterfaceState.subject { + isPinnedMessages = true + } + + var forceInCurrentChat = forceInCurrentChat + if case let .peer(peerId) = self.chatLocation, messageLocation.peerId == peerId, !isPinnedMessages, !isScheduledMessages { + forceInCurrentChat = true + } + + if isPinnedMessages, let messageId = messageLocation.messageId { + let _ = (combineLatest( + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)), + self.context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .local) + |> mapToSignal { result -> Signal<[Message], NoError> in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer, messages in + guard let self, let peer = peer else { + return + } + guard let navigationController = self.effectiveNavigationController else { + return + } + + self.dismiss() + + let navigateToLocation: NavigateToChatControllerParams.Location + if let message = messages.first, let threadId = message.threadId, let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum) { + navigateToLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + } else { + navigateToLocation = .peer(peer) + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: navigateToLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always)) + }) + } else if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) || (isScheduledMessages && messageId.id != 0 && !Namespaces.Message.allScheduled.contains(messageId.namespace)) { + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId), + TelegramEngine.EngineData.Item.Messages.Message(id: messageId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer, message in + guard let self, let peer = peer else { + return + } + if let navigationController = self.effectiveNavigationController { + var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) + if case let .channel(channel) = peer, channel.flags.contains(.isForum), let message = message, let threadId = message.threadId { + chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + } + + var quote: ChatControllerSubject.MessageHighlight.Quote? + if case let .id(_, params) = messageLocation { + quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } + } + + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil), keepStack: .always)) + } + }) + } else if forceInCurrentChat { + if let _ = fromId, let fromIndex = fromIndex, rememberInStack { + self.historyNavigationStack.add(fromIndex) + } + + let scrollFromIndex: MessageIndex? + if let fromIndex = fromIndex { + scrollFromIndex = fromIndex + } else if let message = self.chatDisplayNode.historyNode.lastVisbleMesssage() { + scrollFromIndex = message.index + } else { + scrollFromIndex = nil + } + + if let scrollFromIndex = scrollFromIndex { + if let messageId = messageLocation.messageId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + self.loadingMessage.set(.single(nil)) + self.messageIndexDisposable.set(nil) + + var delayCompletion = true + if self.chatDisplayNode.historyNode.isMessageVisible(id: messageId) { + delayCompletion = false + } + + var quote: (string: String, offset: Int?)? + if case let .id(_, params) = messageLocation { + quote = params.quote.flatMap { quote in (string: quote.string, offset: quote.offset) } + } + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition) + + if delayCompletion { + Queue.mainQueue().after(0.25, { + completion?() + }) + } else { + Queue.mainQueue().justDispatch({ + completion?() + }) + } + + if case let .id(_, params) = messageLocation, let timecode = params.timestamp { + let _ = self.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .timecode(timecode))) + } + } else if case let .index(index) = messageLocation, index.id.id == 0, index.timestamp > 0, case .scheduledMessages = self.presentationInterfaceState.subject { + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition) + } else { + var quote: (string: String, offset: Int?)? + if case let .id(messageId, params) = messageLocation { + if params.timestamp != nil { + self.scheduledScrollToMessageId = (messageId, params) + } + quote = params.quote.flatMap { ($0.string, $0.offset) } + } + var progress: Promise? + if case let .id(_, params) = messageLocation { + progress = params.progress + } + self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) + + let searchLocation: ChatHistoryInitialSearchLocation + switch messageLocation { + case let .id(id, _): + if case let .replyThread(message) = self.chatLocation, id == message.messageId { + searchLocation = .index(.absoluteLowerBound()) + } else { + searchLocation = .id(id) + } + case let .index(index): + searchLocation = .index(index) + case .upperBound: + if let peerId = self.chatLocation.peerId { + searchLocation = .index(MessageIndex.upperBound(peerId: peerId)) + } else { + searchLocation = .index(.absoluteUpperBound()) + } + } + var historyView: Signal + historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) + + var signal: Signal<(MessageIndex?, Bool), NoError> + 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 entry.message.id == messageLocation.messageId { + return .single((entry.message.index, 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) + }) + + /*#if DEBUG + signal = .single((nil, true)) |> then(signal |> delay(2.0, queue: .mainQueue())) + #endif*/ + + var cancelImpl: (() -> Void)? + let presentationData = self.presentationData + let displayTime = CACurrentMediaTime() + let progressSignal = Signal { [weak self] subscriber in + if let progress { + progress.set(.single(true)) + return ActionDisposable { + Queue.mainQueue().async() { + progress.set(.single(false)) + } + } + } else if case .generic = statusSubject { + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + if CACurrentMediaTime() - displayTime > 1.5 { + cancelImpl?() + } + })) + if let customPresentProgress = customPresentProgress { + customPresentProgress(controller, nil) + } else { + self?.present(controller, in: .window(.root)) + } + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } else { + return EmptyDisposable + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.05, queue: Queue.mainQueue()) + let progressDisposable = MetaDisposable() + var progressStarted = false + self.messageIndexDisposable.set((signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + |> deliverOnMainQueue).startStrict(next: { [weak self] index in + if let strongSelf = self, let index = index.0 { + strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, quote: quote, 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(.single(nil)) + } + })) + cancelImpl = { [weak self] in + if let strongSelf = self { + strongSelf.loadingMessage.set(.single(nil)) + strongSelf.messageIndexDisposable.set(nil) + } + } + } + } else { + completion?() + } + } else { + if let fromIndex = fromIndex { + let searchLocation: ChatHistoryInitialSearchLocation + switch messageLocation { + case let .id(id, _): + searchLocation = .id(id) + case let .index(index): + searchLocation = .index(index) + case .upperBound: + return + } + if let _ = fromId, rememberInStack { + self.historyNavigationStack.add(fromIndex) + } + self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) + + var quote: ChatControllerSubject.MessageHighlight.Quote? + if case let .id(_, params) = messageLocation { + quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } + } + + let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote.flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) + var signal: Signal + signal = historyView + |> mapToSignal { historyView -> Signal in + switch historyView { + case .Loading: + return .complete() + case let .HistoryView(view, _, _, _, _, _, _): + for entry in view.entries { + if entry.message.id == messageLocation.messageId { + return .single(entry.message.index) + } + } + return .single(nil) + } + } + |> take(1) + + self.messageIndexDisposable.set((signal |> deliverOnMainQueue).startStrict(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 { + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageLocation.peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let strongSelf = self, let peer = peer else { + return + } + + if let navigationController = strongSelf.effectiveNavigationController { + var quote: ChatControllerSubject.MessageHighlight.Quote? + if case let .id(_, params) = messageLocation { + quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } + } + + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil) })) + } + }) + completion?() + } + } + }, completed: { [weak self] in + if let strongSelf = self { + strongSelf.loadingMessage.set(.single(nil)) + } + })) + } else { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageLocation.peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + guard let self, let peer = peer else { + return + } + if let navigationController = self.effectiveNavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) })) + } + completion?() + }) + } + } + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift new file mode 100644 index 0000000000..0c3692581c --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -0,0 +1,412 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import TelegramNotices +import ContextUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import ReactionSelectionNode +import EntityKeyboard +import TextNodeWithEntities +import PremiumUI +import TooltipUI + +extension ChatControllerImpl { + func openMessageContextMenu(message: Message, selectAll: Bool, node: ASDisplayNode, frame: CGRect, anyRecognizer: UIGestureRecognizer?, location: CGPoint?) -> Void { + if self.presentationInterfaceState.interfaceState.selectionState != nil { + return + } + let presentationData = self.presentationData + + self.dismissAllTooltips() + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = anyRecognizer as? ContextGesture + if let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) { + (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() + self.chatDisplayNode.cancelInteractiveKeyboardGestures() + 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 + } + } + + guard let topMessage = messages.first else { + return + } + + let _ = combineLatest(queue: .mainQueue(), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)), + contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: updatedMessages, controllerInteraction: self.controllerInteraction, selectAll: selectAll, interfaceInteraction: self.interfaceInteraction, messageNode: node as? ChatMessageItemView), + peerMessageAllowedReactions(context: self.context, message: topMessage), + peerMessageSelectedReactions(context: self.context, message: topMessage), + topMessageReactions(context: self.context, message: topMessage), + ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: self.context.sharedContext.accountManager) + ).startStandalone(next: { [weak self] peer, actions, allowedReactions, selectedReactions, topReactions, chatTextSelectionTips in + guard let self else { + return + } + + /*var hasPremium = false + if case let .user(user) = peer, user.isPremium { + hasPremium = true + }*/ + + var actions = actions + switch actions.content { + case let .list(itemList): + if itemList.isEmpty { + return + } + case .custom, .twoLists: + break + } + + var tip: ContextController.Tip? + + if tip == nil { + let isAd = message.adAttribute != nil + + var isAction = false + for media in message.media { + if media is TelegramMediaAction { + isAction = true + break + } + } + if self.presentationInterfaceState.copyProtectionEnabled && !isAction && !isAd { + if case .scheduledMessages = self.subject { + } else { + var isChannel = false + if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info { + isChannel = true + } + tip = .messageCopyProtection(isChannel: isChannel) + } + } else { + let numberOfComponents = message.text.components(separatedBy: CharacterSet.whitespacesAndNewlines).count + let displayTextSelectionTip = numberOfComponents >= 3 && !message.text.isEmpty && chatTextSelectionTips < 3 && !isAd + if displayTextSelectionTip { + let _ = ApplicationSpecificNotice.incrementChatTextSelectionTips(accountManager: self.context.sharedContext.accountManager).startStandalone() + tip = .textSelection + } + } + } + + if actions.tip == nil { + actions.tip = tip + } + + actions.context = self.context + actions.animationCache = self.controllerInteraction?.presentationContext.animationCache + + if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty { + actions.reactionItems = topReactions.map(ReactionContextItem.reaction) + actions.selectedReactionItems = selectedReactions.reactions + + if !actions.reactionItems.isEmpty { + let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in + switch item { + case let .reaction(reaction): + return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation) + default: + return nil + } + } + + var allReactionsAreAvailable = false + switch allowedReactions { + case .set: + allReactionsAreAvailable = false + case .all: + allReactionsAreAvailable = true + } + + if let channel = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info { + allReactionsAreAvailable = false + } + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + if premiumConfiguration.isPremiumDisabled { + allReactionsAreAvailable = false + } + + if allReactionsAreAvailable { + actions.getEmojiContent = { [weak self] animationCache, animationRenderer in + guard let self else { + preconditionFailure() + } + + return EmojiPagerContentComponent.emojiInputData( + context: self.context, + animationCache: animationCache, + animationRenderer: animationRenderer, + isStandalone: false, + subject: .reaction, + hasTrending: false, + topReactionItems: reactionItems, + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: self.chatLocation.peerId, + selectedItems: selectedReactions.files + ) + } + } + } + } + + self.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + + let presentationContext = self.controllerInteraction?.presentationContext + + var disableTransitionAnimations = false + var actionsSignal: Signal = .single(actions) + if let entitiesAttribute = message.textEntitiesAttribute { + var emojiFileIds: [Int64] = [] + for entity in entitiesAttribute.entities { + if case let .CustomEmoji(_, fileId) = entity.type { + emojiFileIds.append(fileId) + } + } + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + + if !emojiFileIds.isEmpty && !premiumConfiguration.isPremiumDisabled { + tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil) + actions.tip = tip + disableTransitionAnimations = true + + let context = self.context + actionsSignal = .single(actions) + |> then( + context.engine.stickers.resolveInlineStickers(fileIds: emojiFileIds) + |> mapToSignal { files -> Signal in + var packReferences: [StickerPackReference] = [] + var existingIds = Set() + for (_, file) in files { + loop: for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { + if case let .id(id, _) = packReference, !existingIds.contains(id) { + packReferences.append(packReference) + existingIds.insert(id) + } + break loop + } + } + } + + let action = { [weak self] in + guard let self else { + return + } + self.presentEmojiList(references: packReferences) + } + + if packReferences.count > 1 { + actions.tip = .animatedEmoji(text: presentationData.strings.ChatContextMenu_EmojiSet(Int32(packReferences.count)), arguments: nil, file: nil, action: action) + return .single(actions) + } else if let reference = packReferences.first { + return context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false) + |> filter { result in + if case .result = result { + return true + } else { + return false + } + } + |> mapToSignal { result in + if case let .result(info, items, _) = result, let presentationContext = presentationContext { + actions.tip = .animatedEmoji( + text: presentationData.strings.ChatContextMenu_EmojiSetSingle(info.title).string, + arguments: TextNodeWithEntities.Arguments( + context: context, + cache: presentationContext.animationCache, + renderer: presentationContext.animationRenderer, + placeholderColor: .clear, + attemptSynchronous: true + ), + file: items.first?.file, + action: action) + return .single(actions) + } else { + return .complete() + } + } + } else { + actions.tip = nil + return .single(actions) + } + } + ) + } + } + + let source: ContextContentSource + if let location = location { + source = .location(ChatMessageContextLocationContentSource(controller: self, location: node.view.convert(node.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) + } else { + source = .extracted(ChatMessageContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll)) + } + + self.canReadHistory.set(false) + + let controller = ContextController(presentationData: self.presentationData, source: source, items: actionsSignal, recognizer: recognizer, gesture: gesture) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + controller.immediateItemsTransitionAnimation = disableTransitionAnimations + controller.getOverlayViews = { [weak self] in + guard let self else { + return [] + } + return [self.chatDisplayNode.navigateButtons.view] + } + self.currentContextController = controller + + controller.premiumReactionsSelected = { [weak self, weak controller] in + guard let self else { + return + } + + controller?.dismissWithoutContent() + + let context = self.context + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumDemoScreen(context: context, subject: .uniqueReactions, action: { + let controller = PremiumIntroScreen(context: context, source: .reactions) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + self.push(controller) + } + + controller.reactionSelected = { [weak self, weak controller] chosenUpdatedReaction, isLarge in + guard let self else { + return + } + + guard let message = messages.first else { + return + } + + controller?.view.endEditing(true) + + let chosenReaction: MessageReaction.Reaction = chosenUpdatedReaction.reaction + + let currentReactions = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] + var updatedReactions: [MessageReaction.Reaction] = currentReactions.filter(\.isSelected).map(\.value) + var removedReaction: MessageReaction.Reaction? + var isFirst = false + + if let index = updatedReactions.firstIndex(where: { $0 == chosenReaction }) { + removedReaction = chosenReaction + updatedReactions.remove(at: index) + } else { + updatedReactions.append(chosenReaction) + isFirst = !currentReactions.contains(where: { $0.value == chosenReaction }) + } + + /*guard let allowedReactions = allowedReactions else { + itemNode.openMessageContextMenu() + return + } + + switch allowedReactions { + case let .set(set): + if !messageAlreadyHasThisReaction && updatedReactions.contains(where: { !set.contains($0) }) { + itemNode.openMessageContextMenu() + return + } + case .all: + break + }*/ + + if removedReaction == nil, case .custom = chosenReaction { + if !self.presentationInterfaceState.isPremium { + controller?.premiumReactionsSelected?() + return + } + } + + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { + if item.message.id == message.id { + if removedReaction == nil && !updatedReactions.isEmpty { + itemNode.awaitingAppliedReaction = (chosenReaction, { [weak self, weak itemNode] in + guard let self, let controller = controller else { + return + } + if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: chosenReaction) { + self.chatDisplayNode.messageTransitionNode.addMessageContextController(messageId: item.message.id, contextController: controller) + + var hideTargetButton: UIView? + if isFirst { + hideTargetButton = targetView.superview + } + + controller.dismissWithReaction(value: chosenReaction, targetView: targetView, hideNode: true, animateTargetContainer: hideTargetButton, addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in + guard let self else { + return + } + self.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + standaloneReactionAnimation.frame = self.chatDisplayNode.bounds + self.chatDisplayNode.addSubnode(standaloneReactionAnimation) + }, completion: { [weak self, weak itemNode, weak targetView] in + guard let self, let itemNode = itemNode, let targetView = targetView else { + return + } + + let _ = self + let _ = itemNode + let _ = targetView + }) + } else { + controller.dismiss() + } + }) + } else { + itemNode.awaitingAppliedReaction = (nil, { + controller?.dismiss() + }) + } + } + } + } + + let mappedUpdatedReactions = updatedReactions.map { reaction -> UpdateMessageReaction in + switch reaction { + case let .builtin(value): + return .builtin(value) + case let .custom(fileId): + var customFile: TelegramMediaFile? + if case let .custom(customFileId, file) = chosenUpdatedReaction, fileId == customFileId { + customFile = file + } + return .custom(fileId: fileId, file: customFile) + } + } + + let _ = updateMessageReactionsInteractively(account: self.context.account, messageId: message.id, reactions: mappedUpdatedReactions, isLarge: isLarge, storeAsRecentlyUsed: true).startStandalone() + } + + self.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + return true + }) + self.window?.presentInGlobalOverlay(controller) + }) + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 29f025f4ab..51102ec5dd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1214,398 +1214,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, openPeerMention: { [weak self] name, progress in self?.openPeerMention(name, progress: progress) }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, anyRecognizer, location in - guard let strongSelf = self, strongSelf.isNodeLoaded else { + guard let self, self.isNodeLoaded else { return } - if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil { - return - } - let presentationData = strongSelf.presentationData - - strongSelf.dismissAllTooltips() - - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer - let gesture: ContextGesture? = anyRecognizer as? ContextGesture - if let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) { - (strongSelf.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() - strongSelf.chatDisplayNode.cancelInteractiveKeyboardGestures() - 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 - } - } - - guard let topMessage = messages.first else { - return - } - - let _ = combineLatest(queue: .mainQueue(), - strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)), - contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction, messageNode: node as? ChatMessageItemView), - peerMessageAllowedReactions(context: strongSelf.context, message: topMessage), - peerMessageSelectedReactions(context: strongSelf.context, message: topMessage), - topMessageReactions(context: strongSelf.context, message: topMessage), - ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) - ).startStandalone(next: { peer, actions, allowedReactions, selectedReactions, topReactions, chatTextSelectionTips in - guard let strongSelf = self else { - return - } - - /*var hasPremium = false - if case let .user(user) = peer, user.isPremium { - hasPremium = true - }*/ - - var actions = actions - switch actions.content { - case let .list(itemList): - if itemList.isEmpty { - return - } - case .custom, .twoLists: - break - } - - var tip: ContextController.Tip? - - if tip == nil { - let isAd = message.adAttribute != nil - - var isAction = false - for media in message.media { - if media is TelegramMediaAction { - isAction = true - break - } - } - if strongSelf.presentationInterfaceState.copyProtectionEnabled && !isAction && !isAd { - if case .scheduledMessages = strongSelf.subject { - } else { - var isChannel = false - if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info { - isChannel = true - } - tip = .messageCopyProtection(isChannel: isChannel) - } - } else { - let numberOfComponents = message.text.components(separatedBy: CharacterSet.whitespacesAndNewlines).count - let displayTextSelectionTip = numberOfComponents >= 3 && !message.text.isEmpty && chatTextSelectionTips < 3 && !isAd - if displayTextSelectionTip { - let _ = ApplicationSpecificNotice.incrementChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone() - tip = .textSelection - } - } - } - - if actions.tip == nil { - actions.tip = tip - } - - actions.context = strongSelf.context - actions.animationCache = strongSelf.controllerInteraction?.presentationContext.animationCache - - if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty { - actions.reactionItems = topReactions.map(ReactionContextItem.reaction) - actions.selectedReactionItems = selectedReactions.reactions - - if !actions.reactionItems.isEmpty { - let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in - switch item { - case let .reaction(reaction): - return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation) - default: - return nil - } - } - - var allReactionsAreAvailable = false - switch allowedReactions { - case .set: - allReactionsAreAvailable = false - case .all: - allReactionsAreAvailable = true - } - - if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info { - allReactionsAreAvailable = false - } - - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - if premiumConfiguration.isPremiumDisabled { - allReactionsAreAvailable = false - } - - if allReactionsAreAvailable { - actions.getEmojiContent = { animationCache, animationRenderer in - guard let strongSelf = self else { - preconditionFailure() - } - - return EmojiPagerContentComponent.emojiInputData( - context: strongSelf.context, - animationCache: animationCache, - animationRenderer: animationRenderer, - isStandalone: false, - subject: .reaction, - hasTrending: false, - topReactionItems: reactionItems, - areUnicodeEmojiEnabled: false, - areCustomEmojiEnabled: true, - chatPeerId: strongSelf.chatLocation.peerId, - selectedItems: selectedReactions.files - ) - } - } - } - } - - strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - - let presentationContext = strongSelf.controllerInteraction?.presentationContext - - var disableTransitionAnimations = false - var actionsSignal: Signal = .single(actions) - if let entitiesAttribute = message.textEntitiesAttribute { - var emojiFileIds: [Int64] = [] - for entity in entitiesAttribute.entities { - if case let .CustomEmoji(_, fileId) = entity.type { - emojiFileIds.append(fileId) - } - } - - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - - if !emojiFileIds.isEmpty && !premiumConfiguration.isPremiumDisabled { - tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil) - actions.tip = tip - disableTransitionAnimations = true - - actionsSignal = .single(actions) - |> then( - context.engine.stickers.resolveInlineStickers(fileIds: emojiFileIds) - |> mapToSignal { files -> Signal in - var packReferences: [StickerPackReference] = [] - var existingIds = Set() - for (_, file) in files { - loop: for attribute in file.attributes { - if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { - if case let .id(id, _) = packReference, !existingIds.contains(id) { - packReferences.append(packReference) - existingIds.insert(id) - } - break loop - } - } - } - - let action = { - guard let strongSelf = self else { - return - } - strongSelf.presentEmojiList(references: packReferences) - } - - if packReferences.count > 1 { - actions.tip = .animatedEmoji(text: presentationData.strings.ChatContextMenu_EmojiSet(Int32(packReferences.count)), arguments: nil, file: nil, action: action) - return .single(actions) - } else if let reference = packReferences.first { - return context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false) - |> filter { result in - if case .result = result { - return true - } else { - return false - } - } - |> mapToSignal { result in - if case let .result(info, items, _) = result, let presentationContext = presentationContext { - actions.tip = .animatedEmoji( - text: presentationData.strings.ChatContextMenu_EmojiSetSingle(info.title).string, - arguments: TextNodeWithEntities.Arguments( - context: context, - cache: presentationContext.animationCache, - renderer: presentationContext.animationRenderer, - placeholderColor: .clear, - attemptSynchronous: true - ), - file: items.first?.file, - action: action) - return .single(actions) - } else { - return .complete() - } - } - } else { - actions.tip = nil - return .single(actions) - } - } - ) - } - } - - let source: ContextContentSource - if let location = location { - source = .location(ChatMessageContextLocationContentSource(controller: strongSelf, location: node.view.convert(node.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) - } else { - source = .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: message, selectAll: selectAll)) - } - - strongSelf.canReadHistory.set(false) - - let controller = ContextController(presentationData: strongSelf.presentationData, source: source, items: actionsSignal, recognizer: recognizer, gesture: gesture) - controller.dismissed = { [weak self] in - self?.canReadHistory.set(true) - } - controller.immediateItemsTransitionAnimation = disableTransitionAnimations - controller.getOverlayViews = { [weak self] in - guard let strongSelf = self else { - return [] - } - return [strongSelf.chatDisplayNode.navigateButtons.view] - } - strongSelf.currentContextController = controller - - controller.premiumReactionsSelected = { [weak controller] in - guard let strongSelf = self else { - return - } - - controller?.dismissWithoutContent() - - let context = strongSelf.context - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumDemoScreen(context: context, subject: .uniqueReactions, action: { - let controller = PremiumIntroScreen(context: context, source: .reactions) - replaceImpl?(controller) - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - strongSelf.push(controller) - } - - controller.reactionSelected = { [weak controller] chosenUpdatedReaction, isLarge in - guard let strongSelf = self else { - return - } - - guard let message = messages.first else { - return - } - - controller?.view.endEditing(true) - - let chosenReaction: MessageReaction.Reaction = chosenUpdatedReaction.reaction - - let currentReactions = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] - var updatedReactions: [MessageReaction.Reaction] = currentReactions.filter(\.isSelected).map(\.value) - var removedReaction: MessageReaction.Reaction? - var isFirst = false - - if let index = updatedReactions.firstIndex(where: { $0 == chosenReaction }) { - removedReaction = chosenReaction - updatedReactions.remove(at: index) - } else { - updatedReactions.append(chosenReaction) - isFirst = !currentReactions.contains(where: { $0.value == chosenReaction }) - } - - /*guard let allowedReactions = allowedReactions else { - itemNode.openMessageContextMenu() - return - } - - switch allowedReactions { - case let .set(set): - if !messageAlreadyHasThisReaction && updatedReactions.contains(where: { !set.contains($0) }) { - itemNode.openMessageContextMenu() - return - } - case .all: - break - }*/ - - if removedReaction == nil, case .custom = chosenReaction { - if !strongSelf.presentationInterfaceState.isPremium { - controller?.premiumReactionsSelected?() - return - } - } - - strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { - if item.message.id == message.id { - if removedReaction == nil && !updatedReactions.isEmpty { - itemNode.awaitingAppliedReaction = (chosenReaction, { [weak itemNode] in - guard let controller = controller else { - return - } - if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: chosenReaction) { - strongSelf.chatDisplayNode.messageTransitionNode.addMessageContextController(messageId: item.message.id, contextController: controller) - - var hideTargetButton: UIView? - if isFirst { - hideTargetButton = targetView.superview - } - - controller.dismissWithReaction(value: chosenReaction, targetView: targetView, hideNode: true, animateTargetContainer: hideTargetButton, addStandaloneReactionAnimation: { standaloneReactionAnimation in - guard let strongSelf = self else { - return - } - strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) - }, completion: { [weak itemNode, weak targetView] in - guard let strongSelf = self, let itemNode = itemNode, let targetView = targetView else { - return - } - - let _ = strongSelf - let _ = itemNode - let _ = targetView - }) - } else { - controller.dismiss() - } - }) - } else { - itemNode.awaitingAppliedReaction = (nil, { - controller?.dismiss() - }) - } - } - } - } - - let mappedUpdatedReactions = updatedReactions.map { reaction -> UpdateMessageReaction in - switch reaction { - case let .builtin(value): - return .builtin(value) - case let .custom(fileId): - var customFile: TelegramMediaFile? - if case let .custom(customFileId, file) = chosenUpdatedReaction, fileId == customFileId { - customFile = file - } - return .custom(fileId: fileId, file: customFile) - } - } - - let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reactions: mappedUpdatedReactions, isLarge: isLarge, storeAsRecentlyUsed: true).startStandalone() - } - - strongSelf.forEachController({ controller in - if let controller = controller as? TooltipScreen { - controller.dismiss() - } - return true - }) - strongSelf.window?.presentInGlobalOverlay(controller) - }) - } + self.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame, anyRecognizer: anyRecognizer, location: location) }, openMessageReactionContextMenu: { [weak self] message, sourceView, gesture, value in guard let strongSelf = self else { return @@ -2026,46 +1638,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let self else { return } - - let continueNavigation: () -> Void = { [weak self] in - guard let self else { - return - } - self.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId) - } - - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: id.peerId) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] toPeer in - guard let self else { - return - } - - if params.quote != nil { - if let toPeer { - switch toPeer { - case let .channel(channel): - if channel.username == nil && channel.usernames.isEmpty { - switch channel.participationStatus { - case .kicked, .left: - self.controllerInteraction?.attemptedNavigationToPrivateQuote(toPeer._asPeer()) - return - case .member: - break - } - } - default: - break - } - } else { - self.controllerInteraction?.attemptedNavigationToPrivateQuote(nil) - return - } - } - - continueNavigation() - }) + self.navigateToMessage(fromId: fromId, id: id, params: params) }, navigateToMessageStandalone: { [weak self] id in self?.navigateToMessage(from: nil, to: .id(id, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: false) }, navigateToThreadMessage: { [weak self] peerId, threadId, messageId in @@ -8114,11 +7687,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current) } } else if let controllerInteraction = strongSelf.controllerInteraction { - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) { + var mappedId = index.id + if index.timestamp == 0 { + if case let .replyThread(message) = strongSelf.chatLocation, let channelMessageId = message.channelMessageId { + mappedId = channelMessageId + } + } + + if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(mappedId) { let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote.flatMap { quote in ChatInterfaceHighlightedState.Quote(string: quote.string, offset: quote.offset) }) controllerInteraction.highlightedState = highlightedState strongSelf.updateItemNodesHighlightedStates(animated: initial) - strongSelf.scrolledToMessageIdValue = ScrolledToMessageId(id: index.id, allowedReplacementDirection: []) + strongSelf.scrolledToMessageIdValue = ScrolledToMessageId(id: mappedId, allowedReplacementDirection: []) var hasQuote = false if let quote = toSubject.quote { @@ -16368,332 +15948,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.navigateToMessage(from: nil, to: messageLocation, scrollPosition: scrollPosition, rememberInStack: false, forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, animated: animated, completion: completion, customPresentProgress: customPresentProgress) } - func navigateToMessage(from fromId: MessageId?, to messageLocation: NavigateToMessageLocation, scrollPosition: ListViewScrollPosition = .center(.bottom), rememberInStack: Bool = true, forceInCurrentChat: Bool = false, dropStack: Bool = false, animated: Bool = true, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil, statusSubject: ChatLoadingMessageSubject = .generic) { - if self.isNodeLoaded { - var fromIndex: MessageIndex? - - if let fromId = fromId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(fromId) { - fromIndex = message.index - } else { - if let message = self.chatDisplayNode.historyNode.anchorMessageInCurrentHistoryView() { - fromIndex = message.index - } - } - - var isScheduledMessages = false - var isPinnedMessages = false - if case .scheduledMessages = self.presentationInterfaceState.subject { - isScheduledMessages = true - } else if case .pinnedMessages = self.presentationInterfaceState.subject { - isPinnedMessages = true - } - - var forceInCurrentChat = forceInCurrentChat - if case let .peer(peerId) = self.chatLocation, messageLocation.peerId == peerId, !isPinnedMessages, !isScheduledMessages { - forceInCurrentChat = true - } - - if isPinnedMessages, let messageId = messageLocation.messageId { - let _ = (combineLatest( - self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)), - self.context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .local) - |> mapToSignal { result -> Signal<[Message], NoError> in - guard case let .result(result) = result else { - return .complete() - } - return .single(result) - } - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer, messages in - guard let self, let peer = peer else { - return - } - guard let navigationController = self.effectiveNavigationController else { - return - } - - self.dismiss() - - let navigateToLocation: NavigateToChatControllerParams.Location - if let message = messages.first, let threadId = message.threadId, let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum) { - navigateToLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) - } else { - navigateToLocation = .peer(peer) - } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: navigateToLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always)) - }) - } else if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) || (isScheduledMessages && messageId.id != 0 && !Namespaces.Message.allScheduled.contains(messageId.namespace)) { - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId), - TelegramEngine.EngineData.Item.Messages.Message(id: messageId) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer, message in - guard let self, let peer = peer else { - return - } - if let navigationController = self.effectiveNavigationController { - var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) - if case let .channel(channel) = peer, channel.flags.contains(.isForum), let message = message, let threadId = message.threadId { - chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) - } - - var quote: ChatControllerSubject.MessageHighlight.Quote? - if case let .id(_, params) = messageLocation { - quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } - } - - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil), keepStack: .always)) - } - }) - } else if forceInCurrentChat { - if let _ = fromId, let fromIndex = fromIndex, rememberInStack { - self.historyNavigationStack.add(fromIndex) - } - - let scrollFromIndex: MessageIndex? - if let fromIndex = fromIndex { - scrollFromIndex = fromIndex - } else if let message = self.chatDisplayNode.historyNode.lastVisbleMesssage() { - scrollFromIndex = message.index - } else { - scrollFromIndex = nil - } - - if let scrollFromIndex = scrollFromIndex { - if let messageId = messageLocation.messageId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - self.loadingMessage.set(.single(nil)) - self.messageIndexDisposable.set(nil) - - var delayCompletion = true - if self.chatDisplayNode.historyNode.isMessageVisible(id: messageId) { - delayCompletion = false - } - - var quote: (string: String, offset: Int?)? - if case let .id(_, params) = messageLocation { - quote = params.quote.flatMap { quote in (string: quote.string, offset: quote.offset) } - } - self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition) - - if delayCompletion { - Queue.mainQueue().after(0.25, { - completion?() - }) - } else { - Queue.mainQueue().justDispatch({ - completion?() - }) - } - - if case let .id(_, params) = messageLocation, let timecode = params.timestamp { - let _ = self.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .timecode(timecode))) - } - } else if case let .index(index) = messageLocation, index.id.id == 0, index.timestamp > 0, case .scheduledMessages = self.presentationInterfaceState.subject { - self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition) - } else { - if case let .id(messageId, params) = messageLocation, params.timestamp != nil { - self.scheduledScrollToMessageId = (messageId, params) - } - var progress: Promise? - if case let .id(_, params) = messageLocation { - progress = params.progress - } - self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) - - let searchLocation: ChatHistoryInitialSearchLocation - switch messageLocation { - case let .id(id, _): - searchLocation = .id(id) - case let .index(index): - searchLocation = .index(index) - case .upperBound: - if let peerId = self.chatLocation.peerId { - searchLocation = .index(MessageIndex.upperBound(peerId: peerId)) - } else { - searchLocation = .index(.absoluteUpperBound()) - } - } - var historyView: Signal - historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) - - var signal: Signal<(MessageIndex?, Bool), NoError> - 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 entry.message.id == messageLocation.messageId { - return .single((entry.message.index, 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) - }) - - /*#if DEBUG - signal = .single((nil, true)) |> then(signal |> delay(2.0, queue: .mainQueue())) - #endif*/ - - var cancelImpl: (() -> Void)? - let presentationData = self.presentationData - let displayTime = CACurrentMediaTime() - let progressSignal = Signal { [weak self] subscriber in - if let progress { - progress.set(.single(true)) - return ActionDisposable { - Queue.mainQueue().async() { - progress.set(.single(false)) - } - } - } else if case .generic = statusSubject { - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - if CACurrentMediaTime() - displayTime > 1.5 { - cancelImpl?() - } - })) - if let customPresentProgress = customPresentProgress { - customPresentProgress(controller, nil) - } else { - self?.present(controller, in: .window(.root)) - } - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } else { - return EmptyDisposable - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.05, queue: Queue.mainQueue()) - let progressDisposable = MetaDisposable() - var progressStarted = false - self.messageIndexDisposable.set((signal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - |> deliverOnMainQueue).startStrict(next: { [weak self] index in - if let strongSelf = self, let index = index.0 { - strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, 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(.single(nil)) - } - })) - cancelImpl = { [weak self] in - if let strongSelf = self { - strongSelf.loadingMessage.set(.single(nil)) - strongSelf.messageIndexDisposable.set(nil) - } - } - } - } else { - completion?() - } - } else { - if let fromIndex = fromIndex { - let searchLocation: ChatHistoryInitialSearchLocation - switch messageLocation { - case let .id(id, _): - searchLocation = .id(id) - case let .index(index): - searchLocation = .index(index) - case .upperBound: - return - } - if let _ = fromId, rememberInStack { - self.historyNavigationStack.add(fromIndex) - } - self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) - - var quote: ChatControllerSubject.MessageHighlight.Quote? - if case let .id(_, params) = messageLocation { - quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } - } - - let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote.flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) - var signal: Signal - signal = historyView - |> mapToSignal { historyView -> Signal in - switch historyView { - case .Loading: - return .complete() - case let .HistoryView(view, _, _, _, _, _, _): - for entry in view.entries { - if entry.message.id == messageLocation.messageId { - return .single(entry.message.index) - } - } - return .single(nil) - } - } - |> take(1) - - self.messageIndexDisposable.set((signal |> deliverOnMainQueue).startStrict(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 { - let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageLocation.peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in - guard let strongSelf = self, let peer = peer else { - return - } - - if let navigationController = strongSelf.effectiveNavigationController { - var quote: ChatControllerSubject.MessageHighlight.Quote? - if case let .id(_, params) = messageLocation { - quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } - } - - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil) })) - } - }) - completion?() - } - } - }, completed: { [weak self] in - if let strongSelf = self { - strongSelf.loadingMessage.set(.single(nil)) - } - })) - } else { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageLocation.peerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - guard let self, let peer = peer else { - return - } - if let navigationController = self.effectiveNavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) })) - } - completion?() - }) - } - } - } else { - completion?() - } - } - func forwardMessages(messageIds: [MessageId], options: ChatInterfaceForwardOptionsState? = nil, resetCurrent: Bool = false) { let _ = (self.context.engine.data.get(EngineDataMap( messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init)