diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index d92d95676f..f4a3b1a459 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1682,12 +1682,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, scrollToTop() } + var displaySearchFilters = true + if strongSelf.chatListDisplayNode.containerNode.mainItemNode.entriesCount < 10 { + displaySearchFilters = false + } + if let searchContentNode = strongSelf.searchContentNode { - if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, navigationController: strongSelf.navigationController as? NavigationController) { + if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, navigationController: strongSelf.navigationController as? NavigationController) { let (filterContainerNode, activate) = filterContainerNodeAndActivate - strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false) - if let parentController = strongSelf.parent as? TabBarController { - parentController.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: true) + if displaySearchFilters { + strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false) + if let parentController = strongSelf.parent as? TabBarController { + parentController.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: true) + } } activate() } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 1341eecf9e..4a47a02090 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -431,6 +431,10 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { return self.currentItemNodeValue!.listNode } + var mainItemNode: ChatListNode { + return self.itemNodes[.all]!.listNode + } + private let currentItemStateValue = Promise<(state: ChatListNodeState, filterId: Int32?)>() var currentItemState: Signal<(state: ChatListNodeState, filterId: Int32?), NoError> { return self.currentItemStateValue.get() @@ -1146,7 +1150,7 @@ final class ChatListControllerNode: ASDisplayNode { } } - func activateSearch(placeholderNode: SearchBarPlaceholderNode, navigationController: NavigationController?) -> (ASDisplayNode, () -> Void)? { + func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, navigationController: NavigationController?) -> (ASDisplayNode, () -> Void)? { guard let (containerLayout, _, _, cleanNavigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else { return nil } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index d4c6ae8a86..b583f77ec3 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -504,7 +504,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case let .message(message, peer, readState, presentationData, totalCount, selected, displayCustomHeader): let header = ChatListSearchItemHeader(type: .messages(totalCount), theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none - if let tagMask = tagMask, tagMask != .photoOrVideo && (searchQuery?.isEmpty ?? true) { + if let tagMask = tagMask, tagMask != .photoOrVideo { return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message, selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: true) } else { return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, filterData: nil, index: ChatListIndex(pinningIndex: nil, messageIndex: message.index), content: .peer(messages: [message], peer: peer, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) @@ -779,9 +779,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.addSubnode(self.recentListNode) self.addSubnode(self.listNode) self.addSubnode(self.mediaNode) - if key != .chats { - self.addSubnode(self.shimmerNode) - } + self.addSubnode(self.shimmerNode) self.addSubnode(self.mediaAccessoryPanelContainer) self.addSubnode(self.emptyResultsAnimationNode) @@ -1285,8 +1283,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let previousSelectedMessages = Atomic?>(value: nil) let _ = (searchQuery - |> deliverOnMainQueue).start(next: { [weak self, weak chatListInteraction] query in + |> deliverOnMainQueue).start(next: { [weak self, weak listInteraction, weak chatListInteraction] query in self?.searchQueryValue = query + listInteraction?.searchTextHighightState = query chatListInteraction?.searchTextHighightState = query }) @@ -1817,7 +1816,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let insets = UIEdgeInsets(top: topPanelHeight, left: sideInset, bottom: bottomInset, right: sideInset) self.shimmerNode.frame = CGRect(origin: CGPoint(x: overflowInset, y: topInset), size: CGSize(width: size.width - overflowInset * 2.0, height: size.height)) - self.shimmerNode.update(context: self.context, size: CGSize(width: size.width - overflowInset * 2.0, height: size.height), presentationData: self.presentationData, key: (self.searchQueryValue?.isEmpty ?? true) ? self.key : .chats, transition: transition) + self.shimmerNode.update(context: self.context, size: CGSize(width: size.width - overflowInset * 2.0, height: size.height), presentationData: self.presentationData, key: !(self.searchQueryValue?.isEmpty ?? true) && self.key == .media ? .chats : self.key, transition: transition) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.recentListNode.frame = CGRect(origin: CGPoint(), size: size) @@ -1995,7 +1994,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.emptyResultsTextNode.isHidden = !emptyResults strongSelf.emptyResultsAnimationNode.visibility = emptyResults - ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateAlpha(node: strongSelf.shimmerNode, alpha: transition.isLoading ? 1.0 : 0.0) + let displayPlaceholder = transition.isLoading && (strongSelf.key != .chats || strongSelf.searchOptionsValue?.peer != nil || strongSelf.searchOptionsValue?.minDate != nil || strongSelf.searchOptionsValue?.maxDate != nil) + ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateAlpha(node: strongSelf.shimmerNode, alpha: displayPlaceholder ? 1.0 : 0.0) strongSelf.recentListNode.isHidden = displayingResults || strongSelf.peersFilter.contains(.excludeRecent) // strongSelf.dimNode.isHidden = displayingResults diff --git a/submodules/ChatListUI/Sources/DateSuggestion.swift b/submodules/ChatListUI/Sources/DateSuggestion.swift index 2c6f80468a..6a85e9b16a 100644 --- a/submodules/ChatListUI/Sources/DateSuggestion.swift +++ b/submodules/ChatListUI/Sources/DateSuggestion.swift @@ -83,8 +83,7 @@ func suggestDates(for string: String, strings: PresentationStrings, dateTimeForm } } else { do { - let dd = try NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue) - if let match = dd.firstMatch(in: string, options: [], range: NSMakeRange(0, string.utf16.count)), let date = match.date, date > telegramReleaseDate { + func process(_ date: Date) { var resultDate = date if resultDate > now && !calendar.isDate(resultDate, equalTo: now, toGranularity: .year) { if let date = calendar.date(byAdding: .year, value: -1, to: resultDate) { @@ -103,6 +102,12 @@ func suggestDates(for string: String, strings: PresentationStrings, dateTimeForm result.append((resultDate, nil)) } } + let dd = try NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue) + if let match = dd.firstMatch(in: string, options: [], range: NSMakeRange(0, string.utf16.count)), let date = match.date, date > telegramReleaseDate { + process(date) + } else if let match = dd.firstMatch(in: string.replacingOccurrences(of: ".", with: "/"), options: [], range: NSMakeRange(0, string.utf16.count)), let date = match.date, date > telegramReleaseDate { + process(date) + } } catch { } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index eba1dbf692..1549710bf8 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -442,6 +442,13 @@ public final class ChatListNode: ListView { private let viewProcessingQueue = Queue() private var chatListView: ChatListNodeView? + var entriesCount: Int { + if let chatListView = self.chatListView { + return chatListView.filteredEntries.count + } else { + return 0 + } + } private var interaction: ChatListNodeInteraction? private var dequeuedInitialTransitionOnLayout = false diff --git a/submodules/ContactListUI/Sources/ContactAddItem.swift b/submodules/ContactListUI/Sources/ContactAddItem.swift index f8af432fbb..7a8699cefc 100644 --- a/submodules/ContactListUI/Sources/ContactAddItem.swift +++ b/submodules/ContactListUI/Sources/ContactAddItem.swift @@ -220,7 +220,7 @@ class ContactsAddItemNode: ListViewItemNode { if let updatedIcon = updatedIcon { strongSelf.iconNode.image = updatedIcon } - transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(x: 14.0, y: 5.0, width: 40.0, height: 40.0)) + transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(x: params.leftInset + 14.0, y: 5.0, width: 40.0, height: 40.0)) let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) diff --git a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift index 5914f34e08..c617854aba 100644 --- a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift @@ -383,7 +383,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index f21fb4ca97..3796afcb0e 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -123,6 +123,28 @@ private enum FileIconImage: Equatable { } } +final class CachedChatListSearchResult { + let text: String + let searchQuery: String + let resultRanges: [Range] + + init(text: String, searchQuery: String, resultRanges: [Range]) { + self.text = text + self.searchQuery = searchQuery + self.resultRanges = resultRanges + } + + func matches(text: String, searchQuery: String) -> Bool { + if self.text != text { + return false + } + if self.searchQuery != searchQuery { + return false + } + return true + } +} + public final class ListMessageFileItemNode: ListMessageNode { private let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode @@ -139,6 +161,7 @@ public final class ListMessageFileItemNode: ListMessageNode { private var selectionNode: ItemListSelectableControlNode? public let titleNode: TextNode + public let textNode: TextNode public let descriptionNode: TextNode private let descriptionProgressNode: ImmediateTextNode public let dateNode: TextNode @@ -170,6 +193,8 @@ public final class ListMessageFileItemNode: ListMessageNode { private var contentSizeValue: CGSize? private var currentLeftOffset: CGFloat = 0.0 + private var cachedChatListSearchResult: CachedChatListSearchResult? + public required init() { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() @@ -189,6 +214,9 @@ public final class ListMessageFileItemNode: ListMessageNode { self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false + + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false self.descriptionNode = TextNode() self.descriptionNode.isUserInteractionEnabled = false @@ -231,6 +259,7 @@ public final class ListMessageFileItemNode: ListMessageNode { self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode) self.offsetContainerNode.addSubnode(self.titleNode) + self.offsetContainerNode.addSubnode(self.textNode) self.offsetContainerNode.addSubnode(self.descriptionNode) self.offsetContainerNode.addSubnode(self.descriptionProgressNode) self.offsetContainerNode.addSubnode(self.dateNode) @@ -307,6 +336,7 @@ public final class ListMessageFileItemNode: ListMessageNode { override public func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode) + let textNodeMakeLayout = TextNode.asyncLayout(self.textNode) let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode) let extensionIconTextMakeLayout = TextNode.asyncLayout(self.extensionIconText) let dateNodeMakeLayout = TextNode.asyncLayout(self.dateNode) @@ -315,6 +345,7 @@ public final class ListMessageFileItemNode: ListMessageNode { let currentMedia = self.currentMedia let currentMessage = self.message let currentIconImage = self.currentIconImage + let currentChatListSearchResult = self.cachedChatListSearchResult let currentItem = self.appliedItem @@ -547,6 +578,36 @@ public final class ListMessageFileItemNode: ListMessageNode { } } + var chatListSearchResult: CachedChatListSearchResult? + if let searchQuery = item.interaction.searchTextHighightState { + if let cached = currentChatListSearchResult, cached.matches(text: item.message.text, searchQuery: searchQuery) { + chatListSearchResult = cached + } else { + let (ranges, text) = findSubstringRanges(in: item.message.text, query: searchQuery) + chatListSearchResult = CachedChatListSearchResult(text: text, searchQuery: searchQuery, resultRanges: ranges) + } + } else { + chatListSearchResult = nil + } + + var captionText: NSMutableAttributedString? + if let chatListSearchResult = chatListSearchResult, !chatListSearchResult.resultRanges.isEmpty, let firstRange = chatListSearchResult.resultRanges.first { + let text = NSMutableAttributedString(string: item.message.text, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) + for range in chatListSearchResult.resultRanges { + let stringRange = NSRange(range, in: chatListSearchResult.text) + if stringRange.location >= 0 && stringRange.location + stringRange.length <= text.length { + text.addAttribute(.foregroundColor, value: item.presentationData.theme.theme.chatList.messageHighlightedTextColor, range: stringRange) + } + } + captionText = text + + let firstRangeOrigin = item.message.text.distance(from: item.message.text.startIndex, to: firstRange.lowerBound) + if firstRangeOrigin > 20 { + captionText = text.attributedSubstring(from: NSMakeRange(firstRangeOrigin - 10, text.length - firstRangeOrigin + 10)).mutableCopy() as? NSMutableAttributedString + captionText?.insert(NSAttributedString(string: "\u{2026}", attributes: [NSAttributedString.Key.font: descriptionFont, NSAttributedString.Key.foregroundColor: item.presentationData.theme.theme.list.itemSecondaryTextColor]), at: 0) + } + } + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message.timestamp, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat) let dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) @@ -555,6 +616,8 @@ public final class ListMessageFileItemNode: ListMessageNode { let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - rightInset - dateNodeLayout.size.width - 4.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textNodeLayout, textNodeApply) = textNodeMakeLayout(TextNodeLayoutArguments(attributedString: captionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(TextNodeLayoutArguments(attributedString: extensionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -600,7 +663,7 @@ public final class ListMessageFileItemNode: ListMessageNode { insets.top += header.height } - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 8.0 * 2.0 + titleNodeLayout.size.height + 3.0 + descriptionNodeLayout.size.height), insets: insets) + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 8.0 * 2.0 + titleNodeLayout.size.height + 3.0 + descriptionNodeLayout.size.height + (textNodeLayout.size.height > 0.0 ? textNodeLayout.size.height + 3.0 : 0.0)), insets: insets) return (nodeLayout, { animation in if let strongSelf = self { @@ -683,7 +746,10 @@ public final class ListMessageFileItemNode: ListMessageNode { } } - transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: strongSelf.titleNode.frame.maxY + 1.0), size: descriptionNodeLayout.size)) + transition.updateFrame(node: strongSelf.textNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: strongSelf.titleNode.frame.maxY + 1.0), size: textNodeLayout.size)) + let _ = textNodeApply() + + transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: strongSelf.titleNode.frame.maxY + 1.0 + (textNodeLayout.size.height > 0.0 ? textNodeLayout.size.height + 3.0 : 0.0)), size: descriptionNodeLayout.size)) let _ = descriptionNodeApply() let _ = dateNodeApply() diff --git a/submodules/ListMessageItem/Sources/ListMessageItem.swift b/submodules/ListMessageItem/Sources/ListMessageItem.swift index 74beffea41..4c51f87395 100644 --- a/submodules/ListMessageItem/Sources/ListMessageItem.swift +++ b/submodules/ListMessageItem/Sources/ListMessageItem.swift @@ -19,6 +19,8 @@ public final class ListMessageItemInteraction { let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void let getHiddenMedia: () -> [MessageId: [Media]] + public var searchTextHighightState: String? + public init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, getHiddenMedia: @escaping () -> [MessageId: [Media]]) { self.openMessage = openMessage self.openMessageContextMenu = openMessageContextMenu diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index 5d21a69ea0..c0453c4db6 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -54,6 +54,8 @@ public final class ListMessageSnippetItemNode: ListMessageNode { private var appliedItem: ListMessageItem? + private var cachedChatListSearchResult: CachedChatListSearchResult? + public required init() { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() @@ -212,6 +214,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { let currentIconImageRepresentation = self.currentIconImageRepresentation let currentItem = self.appliedItem + let currentChatListSearchResult = self.cachedChatListSearchResult let selectionNodeLayout = ItemListSelectableControlNode.asyncLayout(self.selectionNode) @@ -439,6 +442,36 @@ public final class ListMessageSnippetItemNode: ListMessageNode { } } + var chatListSearchResult: CachedChatListSearchResult? + if let searchQuery = item.interaction.searchTextHighightState { + if let cached = currentChatListSearchResult, cached.matches(text: item.message.text, searchQuery: searchQuery) { + chatListSearchResult = cached + } else { + let (ranges, text) = findSubstringRanges(in: item.message.text, query: searchQuery) + chatListSearchResult = CachedChatListSearchResult(text: text, searchQuery: searchQuery, resultRanges: ranges) + } + } else { + chatListSearchResult = nil + } + + if let chatListSearchResult = chatListSearchResult, !chatListSearchResult.resultRanges.isEmpty, let firstRange = chatListSearchResult.resultRanges.first { + var text = NSMutableAttributedString(string: item.message.text, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) + for range in chatListSearchResult.resultRanges { + let stringRange = NSRange(range, in: chatListSearchResult.text) + if stringRange.location >= 0 && stringRange.location + stringRange.length <= text.length { + text.addAttribute(.foregroundColor, value: item.presentationData.theme.theme.chatList.messageHighlightedTextColor, range: stringRange) + } + } + + let firstRangeOrigin = item.message.text.distance(from: item.message.text.startIndex, to: firstRange.lowerBound) + if firstRangeOrigin > 20 { + text = text.attributedSubstring(from: NSMakeRange(firstRangeOrigin - 10, text.length - firstRangeOrigin + 10)).mutableCopy() as! NSMutableAttributedString + text.insert(NSAttributedString(string: "\u{2026}", attributes: [NSAttributedString.Key.font: descriptionFont, NSAttributedString.Key.foregroundColor: item.presentationData.theme.theme.list.itemSecondaryTextColor]), at: 0) + } + + descriptionText = text + } + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message.timestamp, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat) let dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)