diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index d19f931c0c..fa95c60080 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -578,10 +578,12 @@ public enum ChatControllerSubject: Equatable { public struct Quote: Equatable { public let messageId: EngineMessage.Id public let text: String + public let offset: Int? - public init(messageId: EngineMessage.Id, text: String) { + public init(messageId: EngineMessage.Id, text: String, offset: Int?) { self.messageId = messageId self.text = text + self.offset = offset } } @@ -645,9 +647,19 @@ public enum ChatControllerSubject: Equatable { } public struct MessageHighlight: Equatable { - public var quote: String? + public struct Quote: Equatable { + public var string: String + public var offset: Int? + + public init(string: String, offset: Int?) { + self.string = string + self.offset = offset + } + } - public init(quote: String? = nil) { + public var quote: Quote? + + public init(quote: Quote? = nil) { self.quote = quote } } diff --git a/submodules/AccountContext/Sources/ChatHistoryLocation.swift b/submodules/AccountContext/Sources/ChatHistoryLocation.swift index f85a351a72..5d84475d93 100644 --- a/submodules/AccountContext/Sources/ChatHistoryLocation.swift +++ b/submodules/AccountContext/Sources/ChatHistoryLocation.swift @@ -8,20 +8,40 @@ public enum ChatHistoryInitialSearchLocation: Equatable { } public struct MessageHistoryScrollToSubject: Equatable { - public var index: MessageHistoryAnchorIndex - public var quote: String? + public struct Quote: Equatable { + public var string: String + public var offset: Int? + + public init(string: String, offset: Int?) { + self.string = string + self.offset = offset + } + } - public init(index: MessageHistoryAnchorIndex, quote: String?) { + public var index: MessageHistoryAnchorIndex + public var quote: Quote? + + public init(index: MessageHistoryAnchorIndex, quote: Quote?) { self.index = index self.quote = quote } } public struct MessageHistoryInitialSearchSubject: Equatable { - public var location: ChatHistoryInitialSearchLocation - public var quote: String? + public struct Quote: Equatable { + public var string: String + public var offset: Int? + + public init(string: String, offset: Int?) { + self.string = string + self.offset = offset + } + } - public init(location: ChatHistoryInitialSearchLocation, quote: String?) { + public var location: ChatHistoryInitialSearchLocation + public var quote: Quote? + + public init(location: ChatHistoryInitialSearchLocation, quote: Quote?) { self.location = location self.quote = quote } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index c4d4e93fb8..7ea7fa2482 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -2043,7 +2043,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { return .optionalAction({ - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil)) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil)) }) } else if let attribute = attribute as? ReplyStoryAttribute { return .optionalAction({ diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 5f870a77b0..b861a9b1c6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -563,7 +563,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var backgroundType: ChatMessageBackgroundType? private struct HighlightedState: Equatable { - var quote: String? + var quote: ChatInterfaceHighlightedState.Quote? } private var highlightedState: HighlightedState? @@ -4055,7 +4055,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let replyInfoNode = self.replyInfoNode { progress = replyInfoNode.makeProgress() } - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil, progress: progress)) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil, progress: progress)) }, contextMenuOnLongPress: true)) } else if let attribute = attribute as? ReplyStoryAttribute { return .action(InternalBubbleTapAction.Action({ @@ -4692,7 +4692,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI for contentNode in self.contentNodes { if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { - contentNode.updateQuoteTextHighlightState(text: nil, color: .clear, animated: true) + contentNode.updateQuoteTextHighlightState(text: nil, offset: nil, color: .clear, animated: true) } } @@ -4745,7 +4745,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var quoteFrame: CGRect? for contentNode in self.contentNodes { if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { - contentNode.updateQuoteTextHighlightState(text: quote, color: highlightColor, animated: false) + contentNode.updateQuoteTextHighlightState(text: quote.string, offset: quote.offset, color: highlightColor, animated: false) var sourceFrame = backgroundHighlightNode.view.convert(backgroundHighlightNode.bounds, to: contentNode.view) if item.message.effectivelyIncoming(item.context.account.peerId) { sourceFrame.origin.x += 6.0 @@ -5186,10 +5186,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return nil } - public func getQuoteRect(quote: String) -> CGRect? { + public func getQuoteRect(quote: String, offset: Int?) -> CGRect? { for contentNode in self.contentNodes { if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { - if let result = contentNode.getQuoteRect(quote: quote) { + if let result = contentNode.getQuoteRect(quote: quote, offset: offset) { return contentNode.view.convert(result, to: self.view) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index 92dc14d03c..b4345b6166 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -963,7 +963,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { return .optionalAction({ - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil)) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil)) }) } else if let attribute = attribute as? QuotedReplyMessageAttribute { return .action(InternalBubbleTapAction.Action { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 84e8df304a..6980d7ec0e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -1345,7 +1345,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil)) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil)) return } else if let attribute = attribute as? ReplyStoryAttribute { item.controllerInteraction.navigateToStory(item.message, attribute.storyId) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 733f0a9234..3c15eb6de6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -1371,7 +1371,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { return .optionalAction({ - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil)) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil)) }) } else if let attribute = attribute as? ReplyStoryAttribute { return .optionalAction({ diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 100fff36e7..8d70afd66b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -53,6 +53,34 @@ private final class CachedChatMessageText { } } +private func findQuoteRange(string: String, quoteText: String, offset: Int?) -> NSRange? { + let nsString = string as NSString + var currentRange: NSRange? + while true { + let startOffset = currentRange?.upperBound ?? 0 + let range = nsString.range(of: quoteText, range: NSRange(location: startOffset, length: nsString.length - startOffset)) + if range.location != NSNotFound { + if let offset { + if let currentRangeValue = currentRange { + if abs(range.location - offset) > abs(currentRangeValue.location - offset) { + break + } else { + currentRange = range + } + } else { + currentRange = range + } + } else { + currentRange = range + break + } + } else { + break + } + } + return currentRange +} + public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private let containerNode: ASDisplayNode private let textNode: TextNodeWithEntities @@ -1109,15 +1137,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return nil } - public func getQuoteRect(quote: String) -> CGRect? { + public func getQuoteRect(quote: String, offset: Int?) -> CGRect? { var rectsSet: [CGRect] = [] if !quote.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string { - let nsString = string as NSString - let range = nsString.range(of: quote) - if range.location != NSNotFound { - if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { - rectsSet = rects - } + + let range = findQuoteRange(string: string, quoteText: quote, offset: offset) + if let range, let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { + rectsSet = rects } } if !rectsSet.isEmpty { @@ -1136,15 +1162,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return nil } - public func updateQuoteTextHighlightState(text: String?, color: UIColor, animated: Bool) { + public func updateQuoteTextHighlightState(text: String?, offset: Int?, color: UIColor, animated: Bool) { var rectsSet: [CGRect] = [] if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string { - let nsString = string as NSString - let range = nsString.range(of: text) - if range.location != NSNotFound { - if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { - rectsSet = rects - } + + let quoteRange = findQuoteRange(string: string, quoteText: text, offset: offset) + if let quoteRange, let rects = cachedLayout.rangeRects(in: quoteRange)?.rects, !rects.isEmpty { + rectsSet = rects } } if !rectsSet.isEmpty { @@ -1339,12 +1363,12 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private func getSelectionState(range: NSRange?) -> ChatControllerSubject.MessageOptionsInfo.SelectionState { var quote: ChatControllerSubject.MessageOptionsInfo.Quote? if let item = self.item, let range, let selection = self.getCurrentTextSelection(customRange: range) { - quote = ChatControllerSubject.MessageOptionsInfo.Quote(messageId: item.message.id, text: selection.text) + quote = ChatControllerSubject.MessageOptionsInfo.Quote(messageId: item.message.id, text: selection.text, offset: selection.offset) } return ChatControllerSubject.MessageOptionsInfo.SelectionState(canQuote: true, quote: quote) } - public func getCurrentTextSelection(customRange: NSRange? = nil) -> (text: String, entities: [MessageTextEntity])? { + public func getCurrentTextSelection(customRange: NSRange? = nil) -> (text: String, entities: [MessageTextEntity], offset: Int)? { guard let textSelectionNode = self.textSelectionNode else { return nil } @@ -1359,13 +1383,14 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } let nsString = string.string as NSString let substring = nsString.substring(with: range) + let offset = range.location var entities: [MessageTextEntity] = [] if let textEntitiesAttribute = item.message.textEntitiesAttribute { entities = messageTextEntitiesInRange(entities: textEntitiesAttribute.entities, range: range, onlyQuoteable: true) } - return (substring, entities) + return (substring, entities, offset) } public func animateClippingTransition(offset: CGFloat, animation: ListViewItemUpdateAnimation) { diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index a1857ed653..e5c5c499ac 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -19,10 +19,20 @@ import AnimationCache import MultiAnimationRenderer public struct ChatInterfaceHighlightedState: Equatable { - public let messageStableId: UInt32 - public let quote: String? + public struct Quote: Equatable { + public var string: String + public var offset: Int? + + public init(string: String, offset: Int?) { + self.string = string + self.offset = offset + } + } - public init(messageStableId: UInt32, quote: String?) { + public let messageStableId: UInt32 + public let quote: Quote? + + public init(messageStableId: UInt32, quote: Quote?) { self.messageStableId = messageStableId self.quote = quote } @@ -74,11 +84,21 @@ public protocol ChatMessageTransitionProtocol: ASDisplayNode { } public struct NavigateToMessageParams { + public struct Quote { + public var string: String + public var offset: Int? + + public init(string: String, offset: Int?) { + self.string = string + self.offset = offset + } + } + public var timestamp: Double? - public var quote: String? + public var quote: Quote? public var progress: Promise? - public init(timestamp: Double?, quote: String?, progress: Promise? = nil) { + public init(timestamp: Double?, quote: Quote?, progress: Promise? = nil) { self.timestamp = timestamp self.quote = quote self.progress = progress diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index 8996d321cf..435ae807dc 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -302,7 +302,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch var quote: EngineMessageReplyQuote? let trimmedText = trimStringWithEntities(string: textSelection.text, entities: textSelection.entities, maxLength: quoteMaxLength(appConfig: selfController.context.currentAppConfiguration.with({ $0 }))) if !trimmedText.string.isEmpty { - quote = EngineMessageReplyQuote(text: trimmedText.string, offset: nil, entities: trimmedText.entities, media: nil) + quote = EngineMessageReplyQuote(text: trimmedText.string, offset: textSelection.offset, entities: trimmedText.entities, media: nil) } selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: quote)).withoutSelectionState() }) }) @@ -367,7 +367,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch var quote: EngineMessageReplyQuote? let trimmedText = trimStringWithEntities(string: textSelection.text, entities: textSelection.entities, maxLength: quoteMaxLength(appConfig: selfController.context.currentAppConfiguration.with({ $0 }))) if !trimmedText.string.isEmpty { - quote = EngineMessageReplyQuote(text: trimmedText.string, offset: nil, entities: trimmedText.entities, media: nil) + quote = EngineMessageReplyQuote(text: trimmedText.string, offset: textSelection.offset, entities: trimmedText.entities, media: nil) } selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: quote)).withoutSelectionState() }) }) @@ -492,7 +492,7 @@ private func chatReplyOptions(selfController: ChatControllerImpl, sourceNode: AS var replyQuote: ChatControllerSubject.MessageOptionsInfo.Quote? if let quote = replySubject.quote { - replyQuote = ChatControllerSubject.MessageOptionsInfo.Quote(messageId: replySubject.messageId, text: quote.text) + replyQuote = ChatControllerSubject.MessageOptionsInfo.Quote(messageId: replySubject.messageId, text: quote.text, offset: quote.offset) } selectionState.set(selfController.context.account.postbox.messagesAtIds([replySubject.messageId]) |> map { messages -> ChatControllerSubject.MessageOptionsInfo.SelectionState in diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 594153cdd1..fb8f554ec5 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -794,7 +794,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .pinnedMessageUpdated: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil))) + strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil))) break } } @@ -803,7 +803,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .gameScore: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote?.text : nil))) + strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil))) break } } @@ -3893,7 +3893,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let trimmedText = trimStringWithEntities(string: quoteText, entities: messageTextEntitiesInRange(entities: message.textEntitiesAttribute?.entities ?? [], range: nsRange, onlyQuoteable: true), maxLength: quoteMaxLength(appConfig: strongSelf.context.currentAppConfiguration.with({ $0 }))) if !trimmedText.string.isEmpty { - quoteData = EngineMessageReplyQuote(text: trimmedText.string, offset: nil, entities: trimmedText.entities, media: nil) + quoteData = EngineMessageReplyQuote(text: trimmedText.string, offset: nsRange.location, entities: trimmedText.entities, media: nil) } let replySubject = ChatInterfaceState.ReplyMessageSubject( @@ -8070,14 +8070,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } else if let controllerInteraction = strongSelf.controllerInteraction { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) { - let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote) + 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: []) var hasQuote = false if let quote = toSubject.quote { - if message.text.contains(quote) { + if message.text.contains(quote.string) { hasQuote = true } else { strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Chat_ToastQuoteNotFound, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current) @@ -16392,9 +16392,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G 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: String? + var quote: ChatControllerSubject.MessageHighlight.Quote? if case let .id(_, params) = messageLocation { - quote = params.quote + 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)) @@ -16424,9 +16424,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G delayCompletion = false } - var quote: String? + var quote: (string: String, offset: Int?)? if case let .id(_, params) = messageLocation { - quote = params.quote + 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) @@ -16579,12 +16579,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) - var quote: String? + var quote: ChatControllerSubject.MessageHighlight.Quote? if case let .id(_, params) = messageLocation { - quote = params.quote + 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), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) + 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 @@ -16615,9 +16615,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let navigationController = strongSelf.effectiveNavigationController { - var quote: String? + var quote: ChatControllerSubject.MessageHighlight.Quote? if case let .id(_, params) = messageLocation { - quote = params.quote + 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) })) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 6de29374e5..cd41562cc8 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -504,7 +504,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return (messages, Int32(messages.count), false) } - source = .custom(messages: messages, messageId: messageIds.first ?? MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: reply.quote?.text, loadMore: nil) + source = .custom(messages: messages, messageId: messageIds.first ?? MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: reply.quote.flatMap { quote in ChatHistoryListSource.Quote(text: quote.text, offset: quote.offset) }, loadMore: nil) case let .link(link): let messages = link.options |> mapToSignal { options -> Signal<(ChatControllerSubject.LinkOptions, Peer, Message?, [StoryId: CodableEntry]), NoError> in diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 2a6c8ce74c..1d1bc02292 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -417,8 +417,18 @@ private struct ChatHistoryAnimatedEmojiConfiguration { private var nextClientId: Int32 = 1 public enum ChatHistoryListSource { + public struct Quote { + public var text: String + public var offset: Int? + + public init(text: String, offset: Int?) { + self.text = text + self.offset = offset + } + } + case `default` - case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, quote: String?, loadMore: (() -> Void)?) + case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, quote: Quote?, loadMore: (() -> Void)?) } public final class ChatHistoryListNode: ListView, ChatHistoryNode { @@ -818,7 +828,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { initialSearchLocation = .index(MessageIndex.absoluteUpperBound()) } } - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: highlight?.quote), count: historyMessageCount, highlight: highlight != nil), id: 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil), id: 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true), id: 0) } else { @@ -1098,7 +1108,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let scrollPosition: ChatHistoryViewScrollPosition? if isFirstTime, let messageIndex = messages.first(where: { $0.id == at })?.index { - scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(messageIndex), quote: quote), position: .center(.bottom), directionHint: .Down, animated: false, highlight: false, displayLink: false) + scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(messageIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.text, offset: quote.offset) }), position: .center(.bottom), directionHint: .Down, animated: false, highlight: false, displayLink: false) isFirstTime = false } else { scrollPosition = nil @@ -1376,7 +1386,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { initialSearchLocation = .index(.absoluteUpperBound()) } } - strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: highlight?.quote), count: historyMessageCount, highlight: highlight != nil), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if var chatHistoryLocation = strongSelf.chatHistoryLocationValue { @@ -2632,8 +2642,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: String? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom)) { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight), id: self.takeNextHistoryLocationId()) + public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: (string: String, offset: Int?)? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom)) { + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight), id: self.takeNextHistoryLocationId()) } public func anchorMessageInCurrentHistoryView() -> Message? { diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index 8d3637a45d..339332657f 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -227,7 +227,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess preloaded = true - return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) + return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } } case let .Navigation(index, anchorIndex, count, _): diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index de96e7dd71..bcba20cc4c 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -96,7 +96,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam if case let .id(messageId) = messageSubject { let navigationController = params.navigationController let animated = params.animated - controller.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: highlight?.quote)), animated: isFirst, completion: { [weak navigationController, weak controller] in + controller.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: (highlight?.quote).flatMap { quote in NavigateToMessageParams.Quote(string: quote.string, offset: quote.offset) })), animated: isFirst, completion: { [weak navigationController, weak controller] in if let navigationController = navigationController, let controller = controller { let _ = navigationController.popToViewController(controller, animated: animated) } diff --git a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift index 21e2e19c5c..fa0fccb2a1 100644 --- a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift +++ b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift @@ -203,7 +203,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie if case .center = position, let quote = scrollSubject.quote { position = .center(.custom({ itemNode in if let itemNode = itemNode as? ChatMessageBubbleItemNode { - if let quoteRect = itemNode.getQuoteRect(quote: quote) { + if let quoteRect = itemNode.getQuoteRect(quote: quote.string, offset: quote.offset) { return quoteRect.midY } }