Precise quote support

This commit is contained in:
Ali 2023-11-04 01:20:34 +04:00
parent 8051e43e4c
commit 2a02a41ac2
16 changed files with 155 additions and 68 deletions

View File

@ -578,10 +578,12 @@ public enum ChatControllerSubject: Equatable {
public struct Quote: Equatable { public struct Quote: Equatable {
public let messageId: EngineMessage.Id public let messageId: EngineMessage.Id
public let text: String 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.messageId = messageId
self.text = text self.text = text
self.offset = offset
} }
} }
@ -645,9 +647,19 @@ public enum ChatControllerSubject: Equatable {
} }
public struct MessageHighlight: 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 self.quote = quote
} }
} }

View File

@ -8,20 +8,40 @@ public enum ChatHistoryInitialSearchLocation: Equatable {
} }
public struct MessageHistoryScrollToSubject: Equatable { public struct MessageHistoryScrollToSubject: Equatable {
public var index: MessageHistoryAnchorIndex public struct Quote: Equatable {
public var quote: String? 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.index = index
self.quote = quote self.quote = quote
} }
} }
public struct MessageHistoryInitialSearchSubject: Equatable { public struct MessageHistoryInitialSearchSubject: Equatable {
public var location: ChatHistoryInitialSearchLocation public struct Quote: Equatable {
public var quote: String? 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.location = location
self.quote = quote self.quote = quote
} }

View File

@ -2043,7 +2043,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute { if let attribute = attribute as? ReplyMessageAttribute {
return .optionalAction({ 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 { } else if let attribute = attribute as? ReplyStoryAttribute {
return .optionalAction({ return .optionalAction({

View File

@ -563,7 +563,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private var backgroundType: ChatMessageBackgroundType? private var backgroundType: ChatMessageBackgroundType?
private struct HighlightedState: Equatable { private struct HighlightedState: Equatable {
var quote: String? var quote: ChatInterfaceHighlightedState.Quote?
} }
private var highlightedState: HighlightedState? private var highlightedState: HighlightedState?
@ -4055,7 +4055,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let replyInfoNode = self.replyInfoNode { if let replyInfoNode = self.replyInfoNode {
progress = replyInfoNode.makeProgress() 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)) }, contextMenuOnLongPress: true))
} else if let attribute = attribute as? ReplyStoryAttribute { } else if let attribute = attribute as? ReplyStoryAttribute {
return .action(InternalBubbleTapAction.Action({ return .action(InternalBubbleTapAction.Action({
@ -4692,7 +4692,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
for contentNode in self.contentNodes { for contentNode in self.contentNodes {
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { 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? var quoteFrame: CGRect?
for contentNode in self.contentNodes { for contentNode in self.contentNodes {
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { 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) var sourceFrame = backgroundHighlightNode.view.convert(backgroundHighlightNode.bounds, to: contentNode.view)
if item.message.effectivelyIncoming(item.context.account.peerId) { if item.message.effectivelyIncoming(item.context.account.peerId) {
sourceFrame.origin.x += 6.0 sourceFrame.origin.x += 6.0
@ -5186,10 +5186,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return nil return nil
} }
public func getQuoteRect(quote: String) -> CGRect? { public func getQuoteRect(quote: String, offset: Int?) -> CGRect? {
for contentNode in self.contentNodes { for contentNode in self.contentNodes {
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { 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) return contentNode.view.convert(result, to: self.view)
} }
} }

View File

@ -963,7 +963,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute { if let attribute = attribute as? ReplyMessageAttribute {
return .optionalAction({ 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 { } else if let attribute = attribute as? QuotedReplyMessageAttribute {
return .action(InternalBubbleTapAction.Action { return .action(InternalBubbleTapAction.Action {

View File

@ -1345,7 +1345,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
if let item = self.item { if let item = self.item {
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute { 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 return
} else if let attribute = attribute as? ReplyStoryAttribute { } else if let attribute = attribute as? ReplyStoryAttribute {
item.controllerInteraction.navigateToStory(item.message, attribute.storyId) item.controllerInteraction.navigateToStory(item.message, attribute.storyId)

View File

@ -1371,7 +1371,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute { if let attribute = attribute as? ReplyMessageAttribute {
return .optionalAction({ 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 { } else if let attribute = attribute as? ReplyStoryAttribute {
return .optionalAction({ return .optionalAction({

View File

@ -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 { public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private let containerNode: ASDisplayNode private let containerNode: ASDisplayNode
private let textNode: TextNodeWithEntities private let textNode: TextNodeWithEntities
@ -1109,15 +1137,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return nil return nil
} }
public func getQuoteRect(quote: String) -> CGRect? { public func getQuoteRect(quote: String, offset: Int?) -> CGRect? {
var rectsSet: [CGRect] = [] var rectsSet: [CGRect] = []
if !quote.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string { 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) let range = findQuoteRange(string: string, quoteText: quote, offset: offset)
if range.location != NSNotFound { if let range, let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty {
if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { rectsSet = rects
rectsSet = rects
}
} }
} }
if !rectsSet.isEmpty { if !rectsSet.isEmpty {
@ -1136,15 +1162,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return nil 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] = [] var rectsSet: [CGRect] = []
if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string { 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) let quoteRange = findQuoteRange(string: string, quoteText: text, offset: offset)
if range.location != NSNotFound { if let quoteRange, let rects = cachedLayout.rangeRects(in: quoteRange)?.rects, !rects.isEmpty {
if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty { rectsSet = rects
rectsSet = rects
}
} }
} }
if !rectsSet.isEmpty { if !rectsSet.isEmpty {
@ -1339,12 +1363,12 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private func getSelectionState(range: NSRange?) -> ChatControllerSubject.MessageOptionsInfo.SelectionState { private func getSelectionState(range: NSRange?) -> ChatControllerSubject.MessageOptionsInfo.SelectionState {
var quote: ChatControllerSubject.MessageOptionsInfo.Quote? var quote: ChatControllerSubject.MessageOptionsInfo.Quote?
if let item = self.item, let range, let selection = self.getCurrentTextSelection(customRange: range) { 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) 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 { guard let textSelectionNode = self.textSelectionNode else {
return nil return nil
} }
@ -1359,13 +1383,14 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
} }
let nsString = string.string as NSString let nsString = string.string as NSString
let substring = nsString.substring(with: range) let substring = nsString.substring(with: range)
let offset = range.location
var entities: [MessageTextEntity] = [] var entities: [MessageTextEntity] = []
if let textEntitiesAttribute = item.message.textEntitiesAttribute { if let textEntitiesAttribute = item.message.textEntitiesAttribute {
entities = messageTextEntitiesInRange(entities: textEntitiesAttribute.entities, range: range, onlyQuoteable: true) entities = messageTextEntitiesInRange(entities: textEntitiesAttribute.entities, range: range, onlyQuoteable: true)
} }
return (substring, entities) return (substring, entities, offset)
} }
public func animateClippingTransition(offset: CGFloat, animation: ListViewItemUpdateAnimation) { public func animateClippingTransition(offset: CGFloat, animation: ListViewItemUpdateAnimation) {

View File

@ -19,10 +19,20 @@ import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
public struct ChatInterfaceHighlightedState: Equatable { public struct ChatInterfaceHighlightedState: Equatable {
public let messageStableId: UInt32 public struct Quote: Equatable {
public let quote: String? 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.messageStableId = messageStableId
self.quote = quote self.quote = quote
} }
@ -74,11 +84,21 @@ public protocol ChatMessageTransitionProtocol: ASDisplayNode {
} }
public struct NavigateToMessageParams { 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 timestamp: Double?
public var quote: String? public var quote: Quote?
public var progress: Promise<Bool>? public var progress: Promise<Bool>?
public init(timestamp: Double?, quote: String?, progress: Promise<Bool>? = nil) { public init(timestamp: Double?, quote: Quote?, progress: Promise<Bool>? = nil) {
self.timestamp = timestamp self.timestamp = timestamp
self.quote = quote self.quote = quote
self.progress = progress self.progress = progress

View File

@ -302,7 +302,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch
var quote: EngineMessageReplyQuote? var quote: EngineMessageReplyQuote?
let trimmedText = trimStringWithEntities(string: textSelection.text, entities: textSelection.entities, maxLength: quoteMaxLength(appConfig: selfController.context.currentAppConfiguration.with({ $0 }))) let trimmedText = trimStringWithEntities(string: textSelection.text, entities: textSelection.entities, maxLength: quoteMaxLength(appConfig: selfController.context.currentAppConfiguration.with({ $0 })))
if !trimmedText.string.isEmpty { 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() }) }) 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? var quote: EngineMessageReplyQuote?
let trimmedText = trimStringWithEntities(string: textSelection.text, entities: textSelection.entities, maxLength: quoteMaxLength(appConfig: selfController.context.currentAppConfiguration.with({ $0 }))) let trimmedText = trimStringWithEntities(string: textSelection.text, entities: textSelection.entities, maxLength: quoteMaxLength(appConfig: selfController.context.currentAppConfiguration.with({ $0 })))
if !trimmedText.string.isEmpty { 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() }) }) 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? var replyQuote: ChatControllerSubject.MessageOptionsInfo.Quote?
if let quote = replySubject.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]) selectionState.set(selfController.context.account.postbox.messagesAtIds([replySubject.messageId])
|> map { messages -> ChatControllerSubject.MessageOptionsInfo.SelectionState in |> map { messages -> ChatControllerSubject.MessageOptionsInfo.SelectionState in

View File

@ -794,7 +794,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .pinnedMessageUpdated: case .pinnedMessageUpdated:
for attribute in message.attributes { for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute { 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 break
} }
} }
@ -803,7 +803,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .gameScore: case .gameScore:
for attribute in message.attributes { for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute { 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 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 }))) 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 { 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( let replySubject = ChatInterfaceState.ReplyMessageSubject(
@ -8070,14 +8070,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} else if let controllerInteraction = strongSelf.controllerInteraction { } else if let controllerInteraction = strongSelf.controllerInteraction {
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) { 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 controllerInteraction.highlightedState = highlightedState
strongSelf.updateItemNodesHighlightedStates(animated: initial) strongSelf.updateItemNodesHighlightedStates(animated: initial)
strongSelf.scrolledToMessageIdValue = ScrolledToMessageId(id: index.id, allowedReplacementDirection: []) strongSelf.scrolledToMessageIdValue = ScrolledToMessageId(id: index.id, allowedReplacementDirection: [])
var hasQuote = false var hasQuote = false
if let quote = toSubject.quote { if let quote = toSubject.quote {
if message.text.contains(quote) { if message.text.contains(quote.string) {
hasQuote = true hasQuote = true
} else { } 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) 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)) 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 { 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)) 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 delayCompletion = false
} }
var quote: String? var quote: (string: String, offset: Int?)?
if case let .id(_, params) = messageLocation { 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) 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())) self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue()))
var quote: String? var quote: ChatControllerSubject.MessageHighlight.Quote?
if case let .id(_, params) = messageLocation { 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<MessageIndex?, NoError> var signal: Signal<MessageIndex?, NoError>
signal = historyView signal = historyView
|> mapToSignal { historyView -> Signal<MessageIndex?, NoError> in |> mapToSignal { historyView -> Signal<MessageIndex?, NoError> in
@ -16615,9 +16615,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
if let navigationController = strongSelf.effectiveNavigationController { if let navigationController = strongSelf.effectiveNavigationController {
var quote: String? var quote: ChatControllerSubject.MessageHighlight.Quote?
if case let .id(_, params) = messageLocation { 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) })) 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) }))

View File

@ -504,7 +504,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
return (messages, Int32(messages.count), false) 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): case let .link(link):
let messages = link.options let messages = link.options
|> mapToSignal { options -> Signal<(ChatControllerSubject.LinkOptions, Peer, Message?, [StoryId: CodableEntry]), NoError> in |> mapToSignal { options -> Signal<(ChatControllerSubject.LinkOptions, Peer, Message?, [StoryId: CodableEntry]), NoError> in

View File

@ -417,8 +417,18 @@ private struct ChatHistoryAnimatedEmojiConfiguration {
private var nextClientId: Int32 = 1 private var nextClientId: Int32 = 1
public enum ChatHistoryListSource { 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 `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 { public final class ChatHistoryListNode: ListView, ChatHistoryNode {
@ -818,7 +828,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
initialSearchLocation = .index(MessageIndex.absoluteUpperBound()) 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 { } 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) self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true), id: 0)
} else { } else {
@ -1098,7 +1108,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let scrollPosition: ChatHistoryViewScrollPosition? let scrollPosition: ChatHistoryViewScrollPosition?
if isFirstTime, let messageIndex = messages.first(where: { $0.id == at })?.index { 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 isFirstTime = false
} else { } else {
scrollPosition = nil scrollPosition = nil
@ -1376,7 +1386,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
initialSearchLocation = .index(.absoluteUpperBound()) 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 { } 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) 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 { } 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)) { 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), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight), id: self.takeNextHistoryLocationId()) 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? { public func anchorMessageInCurrentHistoryView() -> Message? {

View File

@ -227,7 +227,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess
preloaded = true 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, _): case let .Navigation(index, anchorIndex, count, _):

View File

@ -96,7 +96,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
if case let .id(messageId) = messageSubject { if case let .id(messageId) = messageSubject {
let navigationController = params.navigationController let navigationController = params.navigationController
let animated = params.animated 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 { if let navigationController = navigationController, let controller = controller {
let _ = navigationController.popToViewController(controller, animated: animated) let _ = navigationController.popToViewController(controller, animated: animated)
} }

View File

@ -203,7 +203,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie
if case .center = position, let quote = scrollSubject.quote { if case .center = position, let quote = scrollSubject.quote {
position = .center(.custom({ itemNode in position = .center(.custom({ itemNode in
if let itemNode = itemNode as? ChatMessageBubbleItemNode { 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 return quoteRect.midY
} }
} }