Thread quote highlight

This commit is contained in:
Ali
2023-11-10 16:39:12 +04:00
parent cc3ea54e74
commit 843f5a6e91
3 changed files with 836 additions and 758 deletions

View File

@@ -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<Bool>?
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<ChatHistoryViewUpdate, NoError>
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<Never, NoError> { [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<MessageIndex?, NoError>
signal = historyView
|> mapToSignal { historyView -> Signal<MessageIndex?, NoError> 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?()
})
}
}
}
}

View File

@@ -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<ContextController.Items, NoError> = .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<ContextController.Items, NoError> in
var packReferences: [StickerPackReference] = []
var existingIds = Set<Int64>()
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)
})
}
}
}

View File

@@ -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<ContextController.Items, NoError> = .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<ContextController.Items, NoError> in
var packReferences: [StickerPackReference] = []
var existingIds = Set<Int64>()
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<Bool>?
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<ChatHistoryViewUpdate, NoError>
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<Never, NoError> { [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<MessageIndex?, NoError>
signal = historyView
|> mapToSignal { historyView -> Signal<MessageIndex?, NoError> 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)