Topics search

This commit is contained in:
Ilya Laktyushin 2022-10-15 17:44:04 +03:00
parent 20b9eea20f
commit ad2b8811c1
19 changed files with 467 additions and 163 deletions

View File

@ -8132,3 +8132,7 @@ Sorry for the inconvenience.";
"ChatList.CloseAction" = "Close"; "ChatList.CloseAction" = "Close";
"Channel.EditAdmin.PermissionCreateTopics" = "Create Topics"; "Channel.EditAdmin.PermissionCreateTopics" = "Create Topics";
"ChatList.Search.FilterTopics" = "Topics";
"DialogList.SearchSectionTopics" = "Topics";

View File

@ -614,6 +614,7 @@ public final class ContactSelectionControllerParams {
public enum ChatListSearchFilter: Equatable { public enum ChatListSearchFilter: Equatable {
case chats case chats
case topics
case media case media
case downloads case downloads
case links case links
@ -627,18 +628,20 @@ public enum ChatListSearchFilter: Equatable {
switch self { switch self {
case .chats: case .chats:
return 0 return 0
case .media: case .topics:
return 1 return 1
case .downloads: case .media:
return 2 return 2
case .links: case .downloads:
return 3
case .files:
return 4 return 4
case .music: case .links:
return 5 return 5
case .voice: case .files:
return 6 return 6
case .music:
return 7
case .voice:
return 8
case let .peer(peerId, _, _, _): case let .peer(peerId, _, _, _):
return peerId.id._internalGetInt64Value() return peerId.id._internalGetInt64Value()
case let .date(_, date, _): case let .date(_, date, _):

View File

@ -29,6 +29,7 @@ public enum ChatListSearchItemHeaderType {
case subscribers case subscribers
case downloading case downloading
case recentDownloads case recentDownloads
case topics
fileprivate func title(strings: PresentationStrings) -> String { fileprivate func title(strings: PresentationStrings) -> String {
switch self { switch self {
@ -80,6 +81,8 @@ public enum ChatListSearchItemHeaderType {
return strings.DownloadList_DownloadingHeader return strings.DownloadList_DownloadingHeader
case .recentDownloads: case .recentDownloads:
return strings.DownloadList_DownloadedHeader return strings.DownloadList_DownloadedHeader
case .topics:
return strings.DialogList_SearchSectionTopics
} }
} }
@ -133,6 +136,8 @@ public enum ChatListSearchItemHeaderType {
return .downloading return .downloading
case .recentDownloads: case .recentDownloads:
return .recentDownloads return .recentDownloads
case .topics:
return .topics
} }
} }
} }
@ -166,6 +171,7 @@ private enum ChatListSearchItemHeaderId: Int32 {
case subscribers case subscribers
case downloading case downloading
case recentDownloads case recentDownloads
case topics
} }
public final class ChatListSearchItemHeader: ListViewItemHeader { public final class ChatListSearchItemHeader: ListViewItemHeader {

View File

@ -840,7 +840,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if !previewing { if !previewing {
let placeholder: String let placeholder: String
let compactPlaceholder: String let compactPlaceholder: String
var isForum = false
if case .forum = location { if case .forum = location {
isForum = true
placeholder = self.presentationData.strings.Common_Search placeholder = self.presentationData.strings.Common_Search
compactPlaceholder = self.presentationData.strings.Common_Search compactPlaceholder = self.presentationData.strings.Common_Search
} else { } else {
@ -849,7 +852,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: placeholder, compactPlaceholder: compactPlaceholder, activate: { [weak self] in self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: placeholder, compactPlaceholder: compactPlaceholder, activate: { [weak self] in
self?.activateSearch() self?.activateSearch(filter: isForum ? .topics : .chats)
}) })
self.searchContentNode?.updateExpansionProgress(0.0) self.searchContentNode?.updateExpansionProgress(0.0)
self.navigationBar?.setContentNode(self.searchContentNode, animated: false) self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
@ -1429,7 +1432,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
} }
self.chatListDisplayNode.requestOpenMessageFromSearch = { [weak self] peer, messageId, deactivateOnAction in self.chatListDisplayNode.requestOpenMessageFromSearch = { [weak self] peer, threadId, messageId, deactivateOnAction in
if let strongSelf = self { if let strongSelf = self {
strongSelf.openMessageFromSearchDisposable.set((strongSelf.context.engine.peers.ensurePeerIsLocallyAvailable(peer: peer) strongSelf.openMessageFromSearchDisposable.set((strongSelf.context.engine.peers.ensurePeerIsLocallyAvailable(peer: peer)
|> deliverOnMainQueue).start(next: { [weak strongSelf] actualPeerId in |> deliverOnMainQueue).start(next: { [weak strongSelf] actualPeerId in

View File

@ -1127,7 +1127,7 @@ final class ChatListControllerNode: ASDisplayNode {
var requestDeactivateSearch: (() -> Void)? var requestDeactivateSearch: (() -> Void)?
var requestOpenPeerFromSearch: ((EnginePeer, Bool) -> Void)? var requestOpenPeerFromSearch: ((EnginePeer, Bool) -> Void)?
var requestOpenRecentPeerOptions: ((EnginePeer) -> Void)? var requestOpenRecentPeerOptions: ((EnginePeer) -> Void)?
var requestOpenMessageFromSearch: ((EnginePeer, EngineMessage.Id, Bool) -> Void)? var requestOpenMessageFromSearch: ((EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void)?
var requestAddContact: ((String) -> Void)? var requestAddContact: ((String) -> Void)?
var peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? var peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
var dismissSelfIfCompletedPresentation: (() -> Void)? var dismissSelfIfCompletedPresentation: (() -> Void)?
@ -1298,20 +1298,17 @@ final class ChatListControllerNode: ASDisplayNode {
guard let (containerLayout, _, _, cleanNavigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else { guard let (containerLayout, _, _, cleanNavigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else {
return nil return nil
} }
guard case let .chatList(groupId) = self.location else {
return nil
}
let filter: ChatListNodePeersFilter = [] let filter: ChatListNodePeersFilter = []
let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, groupId: groupId, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, dismissSearch in let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, location: location, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, dismissSearch in
self?.requestOpenPeerFromSearch?(peer, dismissSearch) self?.requestOpenPeerFromSearch?(peer, dismissSearch)
}, openDisabledPeer: { _ in }, openDisabledPeer: { _ in
}, openRecentPeerOptions: { [weak self] peer in }, openRecentPeerOptions: { [weak self] peer in
self?.requestOpenRecentPeerOptions?(peer) self?.requestOpenRecentPeerOptions?(peer)
}, openMessage: { [weak self] peer, messageId, deactivateOnAction in }, openMessage: { [weak self] peer, threadId, messageId, deactivateOnAction in
if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch { if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch {
requestOpenMessageFromSearch(peer, messageId, deactivateOnAction) requestOpenMessageFromSearch(peer, threadId, messageId, deactivateOnAction)
} }
}, addContact: { [weak self] phoneNumber in }, addContact: { [weak self] phoneNumber in
if let requestAddContact = self?.requestAddContact { if let requestAddContact = self?.requestAddContact {

View File

@ -36,6 +36,7 @@ import MultiAnimationRenderer
private enum ChatListTokenId: Int32 { private enum ChatListTokenId: Int32 {
case archive case archive
case forum
case filter case filter
case peer case peer
case date case date
@ -44,7 +45,7 @@ private enum ChatListTokenId: Int32 {
final class ChatListSearchInteraction { final class ChatListSearchInteraction {
let openPeer: (EnginePeer, EnginePeer?, Bool) -> Void let openPeer: (EnginePeer, EnginePeer?, Bool) -> Void
let openDisabledPeer: (EnginePeer) -> Void let openDisabledPeer: (EnginePeer) -> Void
let openMessage: (EnginePeer, EngineMessage.Id, Bool) -> Void let openMessage: (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void
let openUrl: (String) -> Void let openUrl: (String) -> Void
let clearRecentSearch: () -> Void let clearRecentSearch: () -> Void
let addContact: (String) -> Void let addContact: (String) -> Void
@ -56,7 +57,7 @@ final class ChatListSearchInteraction {
let dismissInput: () -> Void let dismissInput: () -> Void
let getSelectedMessageIds: () -> Set<EngineMessage.Id>? let getSelectedMessageIds: () -> Set<EngineMessage.Id>?
init(openPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set<EngineMessage.Id>?) { init(openPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set<EngineMessage.Id>?) {
self.openPeer = openPeer self.openPeer = openPeer
self.openDisabledPeer = openDisabledPeer self.openDisabledPeer = openDisabledPeer
self.openMessage = openMessage self.openMessage = openMessage
@ -84,11 +85,11 @@ private struct ChatListSearchContainerNodeSearchState: Equatable {
public final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { public final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
private let context: AccountContext private let context: AccountContext
private let peersFilter: ChatListNodePeersFilter private let peersFilter: ChatListNodePeersFilter
private let groupId: EngineChatList.Group private let location: ChatListControllerLocation
private let displaySearchFilters: Bool private let displaySearchFilters: Bool
private let hasDownloads: Bool private let hasDownloads: Bool
private var interaction: ChatListSearchInteraction? private var interaction: ChatListSearchInteraction?
private let openMessage: (EnginePeer, EngineMessage.Id, Bool) -> Void private let openMessage: (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void
private let navigationController: NavigationController? private let navigationController: NavigationController?
let filterContainerNode: ChatListSearchFiltersContainerNode let filterContainerNode: ChatListSearchFiltersContainerNode
@ -111,6 +112,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
private let suggestedDates = Promise<[(Date?, Date, String?)]>([]) private let suggestedDates = Promise<[(Date?, Date, String?)]>([])
private var suggestedFilters: [ChatListSearchFilter]? private var suggestedFilters: [ChatListSearchFilter]?
private let suggestedFiltersDisposable = MetaDisposable() private let suggestedFiltersDisposable = MetaDisposable()
private var forumPeer: EnginePeer?
private var shareStatusDisposable: MetaDisposable? private var shareStatusDisposable: MetaDisposable?
@ -131,10 +133,10 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
private var validLayout: (ContainerViewLayout, CGFloat)? private var validLayout: (ContainerViewLayout, CGFloat)?
public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter, groupId: EngineChatList.Group, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter, location: ChatListControllerLocation, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) {
self.context = context self.context = context
self.peersFilter = filter self.peersFilter = filter
self.groupId = groupId self.location = location
self.displaySearchFilters = displaySearchFilters self.displaySearchFilters = displaySearchFilters
self.hasDownloads = hasDownloads self.hasDownloads = hasDownloads
self.navigationController = navigationController self.navigationController = navigationController
@ -148,7 +150,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.presentInGlobalOverlay = presentInGlobalOverlay self.presentInGlobalOverlay = presentInGlobalOverlay
self.filterContainerNode = ChatListSearchFiltersContainerNode() self.filterContainerNode = ChatListSearchFiltersContainerNode()
self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, groupId: groupId, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, location: location, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController)
self.paneContainerNode.clipsToBounds = true self.paneContainerNode.clipsToBounds = true
super.init() super.init()
@ -164,8 +166,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
} }
}, openDisabledPeer: { peer in }, openDisabledPeer: { peer in
openDisabledPeer(peer) openDisabledPeer(peer)
}, openMessage: { peer, messageId, deactivateOnAction in }, openMessage: { peer, threadId, messageId, deactivateOnAction in
originalOpenMessage(peer, messageId, deactivateOnAction) originalOpenMessage(peer, threadId, messageId, deactivateOnAction)
if peer.id.namespace != Namespaces.Peer.SecretChat { if peer.id.namespace != Namespaces.Peer.SecretChat {
addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_message", peerId: peer.id, data: .dictionary(["msg_id": .number(Double(messageId.id))])) addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_message", peerId: peer.id, data: .dictionary(["msg_id": .number(Double(messageId.id))]))
} }
@ -248,6 +250,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
switch key { switch key {
case .chats: case .chats:
filterKey = .chats filterKey = .chats
case .topics:
filterKey = .topics
case .media: case .media:
filterKey = .media filterKey = .media
case .downloads: case .downloads:
@ -270,7 +274,12 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty { if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty {
filters = suggestedFilters filters = suggestedFilters
} else { } else {
filters = defaultAvailableSearchPanes(hasDownloads: strongSelf.hasDownloads).map(\.filter) var isForum = false
if case .forum = strongSelf.location {
isForum = true
}
filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: strongSelf.hasDownloads).map(\.filter)
} }
strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilter?.id, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition) strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilter?.id, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition)
} }
@ -289,6 +298,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
switch filter { switch filter {
case .chats: case .chats:
key = .chats key = .chats
case .topics:
key = .topics
case .media: case .media:
key = .media key = .media
case .downloads: case .downloads:
@ -318,38 +329,43 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
let searchQuerySignal = self.searchQuery.get() let searchQuerySignal = self.searchQuery.get()
let suggestedPeers = self.selectedFilterPromise.get() let suggestedPeers: Signal<[EnginePeer], NoError>
|> map { filter -> Bool in if case .chatList = location {
guard let filter = filter else { suggestedPeers = self.selectedFilterPromise.get()
return false |> map { filter -> Bool in
} guard let filter = filter else {
switch filter {
case let .filter(filter):
switch filter {
case .downloads:
return false return false
default: }
return true switch filter {
case let .filter(filter):
switch filter {
case .downloads:
return false
default:
return true
}
} }
} }
} |> distinctUntilChanged
|> distinctUntilChanged |> mapToSignal { value -> Signal<String?, NoError> in
|> mapToSignal { value -> Signal<String?, NoError> in if value {
if value { return searchQuerySignal
return searchQuerySignal } else {
} else { return .single(nil)
return .single(nil)
}
}
|> mapToSignal { query -> Signal<[EnginePeer], NoError> in
if let query = query {
return context.account.postbox.searchPeers(query: query.lowercased())
|> map { local -> [EnginePeer] in
return Array(local.compactMap { $0.peer }.prefix(10).map(EnginePeer.init))
} }
} else {
return .single([])
} }
|> mapToSignal { query -> Signal<[EnginePeer], NoError> in
if let query = query {
return context.account.postbox.searchPeers(query: query.lowercased())
|> map { local -> [EnginePeer] in
return Array(local.compactMap { $0.peer }.prefix(10).map(EnginePeer.init))
}
} else {
return .single([])
}
}
} else {
suggestedPeers = .single([])
} }
let accountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) let accountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
@ -446,6 +462,14 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
} }
}) })
if case let .forum(peerId) = location {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
self?.forumPeer = peer
self?.updateSearchOptions(nil)
})
}
self._ready.set(self.paneContainerNode.isReady.get() self._ready.set(self.paneContainerNode.isReady.get()
|> map { _ in Void() }) |> map { _ in Void() })
} }
@ -493,8 +517,10 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
private func updateSearchOptions(_ options: ChatListSearchOptions?, clearQuery: Bool = false) { private func updateSearchOptions(_ options: ChatListSearchOptions?, clearQuery: Bool = false) {
var options = options var options = options
var tokens: [SearchBarToken] = [] var tokens: [SearchBarToken] = []
if self.groupId == .archive { if case .chatList(.archive) = self.location {
tokens.append(SearchBarToken(id: ChatListTokenId.archive.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Archive"), iconOffset: -1.0, title: self.presentationData.strings.ChatList_Archive, permanent: true)) tokens.append(SearchBarToken(id: ChatListTokenId.archive.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Archive"), iconOffset: -1.0, title: self.presentationData.strings.ChatList_Archive, permanent: true))
} else if case .forum = self.location, let forumPeer = self.forumPeer {
tokens.append(SearchBarToken(id: ChatListTokenId.forum.rawValue, icon: nil, iconOffset: -1.0, peer: (forumPeer, self.context, self.presentationData.theme), title: self.presentationData.strings.ChatList_Archive, permanent: true))
} }
if options?.isEmpty ?? true { if options?.isEmpty ?? true {
@ -547,6 +573,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
public func search(filter: ChatListSearchFilter, query: String?) { public func search(filter: ChatListSearchFilter, query: String?) {
let key: ChatListSearchPaneKey let key: ChatListSearchPaneKey
switch filter { switch filter {
case .topics:
key = .topics
case .media: case .media:
key = .media key = .media
case .links: case .links:
@ -567,8 +595,10 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.searchTextUpdated(text: query ?? "") self.searchTextUpdated(text: query ?? "")
var tokens: [SearchBarToken] = [] var tokens: [SearchBarToken] = []
if self.groupId == .archive { if case .chatList(.archive) = self.location {
tokens.append(SearchBarToken(id: ChatListTokenId.archive.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Archive"), iconOffset: -1.0, title: self.presentationData.strings.ChatList_Archive, permanent: true)) tokens.append(SearchBarToken(id: ChatListTokenId.archive.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Archive"), iconOffset: -1.0, title: self.presentationData.strings.ChatList_Archive, permanent: true))
} else if case .forum = self.location, let forumPeer = self.forumPeer {
tokens.append(SearchBarToken(id: ChatListTokenId.forum.rawValue, icon: nil, iconOffset: -1.0, peer: (forumPeer, self.context, self.presentationData.theme), title: self.presentationData.strings.ChatList_Archive, permanent: true))
} }
self.setQuery?(nil, tokens, query ?? "") self.setQuery?(nil, tokens, query ?? "")
} }
@ -581,18 +611,23 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
let topInset = navigationBarHeight let topInset = navigationBarHeight
transition.updateFrame(node: self.filterContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight + 6.0), size: CGSize(width: layout.size.width, height: 38.0))) transition.updateFrame(node: self.filterContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight + 6.0), size: CGSize(width: layout.size.width, height: 38.0)))
var isForum = false
if case .forum = self.location {
isForum = true
}
let filters: [ChatListSearchFilter] let filters: [ChatListSearchFilter]
if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty { if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty {
filters = suggestedFilters filters = suggestedFilters
} else { } else {
filters = defaultAvailableSearchPanes(hasDownloads: self.hasDownloads).map(\.filter) filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads).map(\.filter)
} }
let overflowInset: CGFloat = 20.0 let overflowInset: CGFloat = 20.0
self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
var bottomIntrinsicInset = layout.intrinsicInsets.bottom var bottomIntrinsicInset = layout.intrinsicInsets.bottom
if case .root = self.groupId { if case .chatList(.root) = self.location {
if layout.safeInsets.left > overflowInset { if layout.safeInsets.left > overflowInset {
bottomIntrinsicInset -= 34.0 bottomIntrinsicInset -= 34.0
} else { } else {
@ -752,15 +787,15 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
bottomInset = inputHeight bottomInset = inputHeight
} else if let _ = self.selectionPanelNode { } else if let _ = self.selectionPanelNode {
bottomInset = bottomIntrinsicInset bottomInset = bottomIntrinsicInset
} else if case .root = self.groupId { } else if case .chatList(.root) = self.location {
bottomInset -= bottomIntrinsicInset bottomInset -= bottomIntrinsicInset
} }
let availablePanes: [ChatListSearchPaneKey] let availablePanes: [ChatListSearchPaneKey]
if self.displaySearchFilters { if self.displaySearchFilters {
availablePanes = defaultAvailableSearchPanes(hasDownloads: self.hasDownloads) availablePanes = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads)
} else { } else {
availablePanes = [.chats] availablePanes = isForum ? [.topics] : [.chats]
} }
self.paneContainerNode.update(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), sideInset: layout.safeInsets.left, bottomInset: bottomInset, visibleHeight: layout.size.height - topInset, presentationData: self.presentationData, availablePanes: availablePanes, transition: transition) self.paneContainerNode.update(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), sideInset: layout.safeInsets.left, bottomInset: bottomInset, visibleHeight: layout.size.height - topInset, presentationData: self.presentationData, availablePanes: availablePanes, transition: transition)
@ -885,7 +920,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss(completion: { [weak self] in c.dismiss(completion: { [weak self] in
self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), message.id, false) self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), nil, message.id, false)
}) })
}))) })))
@ -964,7 +999,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
} }
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss(completion: { [weak self] in c.dismiss(completion: { [weak self] in
self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), message.id, false) self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), nil, message.id, false)
}) })
}))) })))
@ -1010,7 +1045,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, f in items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, f in
c.dismiss(completion: { c.dismiss(completion: {
self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), message.id, false) self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), nil, message.id, false)
}) })
}))) })))
@ -1062,7 +1097,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
public override func searchTextClearTokens() { public override func searchTextClearTokens() {
self.updateSearchOptions(nil) self.updateSearchOptions(nil)
self.setQuery?(nil, [], self.searchQueryValue ?? "") // self.setQuery?(nil, [], self.searchQueryValue ?? "")
} }
func deleteMessages(messageIds: Set<EngineMessage.Id>?) { func deleteMessages(messageIds: Set<EngineMessage.Id>?) {

View File

@ -81,6 +81,9 @@ private final class ItemNode: ASDisplayNode {
case .chats: case .chats:
title = presentationData.strings.ChatList_Search_FilterChats title = presentationData.strings.ChatList_Search_FilterChats
icon = nil icon = nil
case .topics:
title = presentationData.strings.ChatList_Search_FilterTopics
icon = nil
case .media: case .media:
title = presentationData.strings.ChatList_Search_FilterMedia title = presentationData.strings.ChatList_Search_FilterMedia
icon = nil icon = nil

View File

@ -239,6 +239,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
} }
public enum ChatListSearchEntryStableId: Hashable { public enum ChatListSearchEntryStableId: Hashable {
case threadId(Int64)
case localPeerId(EnginePeer.Id) case localPeerId(EnginePeer.Id)
case globalPeerId(EnginePeer.Id) case globalPeerId(EnginePeer.Id)
case messageId(EngineMessage.Id, ChatListSearchEntry.MessageSection) case messageId(EngineMessage.Id, ChatListSearchEntry.MessageSection)
@ -297,21 +298,24 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case recentlyDownloaded case recentlyDownloaded
} }
case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType)
case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder) case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder)
case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType)
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType)
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool) case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool)
case addContact(String, PresentationTheme, PresentationStrings) case addContact(String, PresentationTheme, PresentationStrings)
public var stableId: ChatListSearchEntryStableId { public var stableId: ChatListSearchEntryStableId {
switch self { switch self {
case let .topic(_, threadInfo, _, _, _, _):
return .threadId(threadInfo.id)
case let .recentlySearchedPeer(peer, _, _, _, _, _, _, _): case let .recentlySearchedPeer(peer, _, _, _, _, _, _, _):
return .localPeerId(peer.id) return .localPeerId(peer.id)
case let .localPeer(peer, _, _, _, _, _, _, _, _): case let .localPeer(peer, _, _, _, _, _, _, _, _):
return .localPeerId(peer.id) return .localPeerId(peer.id)
case let .globalPeer(peer, _, _, _, _, _, _, _): case let .globalPeer(peer, _, _, _, _, _, _, _):
return .globalPeerId(peer.peer.id) return .globalPeerId(peer.peer.id)
case let .message(message, _, _, _, _, _, _, _, _, section, _): case let .message(message, _, _, _, _, _, _, _, _, _, section, _):
return .messageId(message.id, section) return .messageId(message.id, section)
case .addContact: case .addContact:
return .addContact return .addContact
@ -320,6 +324,12 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
public static func ==(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool { public static func ==(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool {
switch lhs { switch lhs {
case let .topic(lhsPeer, lhsThreadInfo, lhsIndex, lhsTheme, lhsStrings, lhsExpandType):
if case let .topic(rhsPeer, rhsThreadInfo, rhsIndex, rhsTheme, rhsStrings, rhsExpandType) = rhs, lhsPeer == rhsPeer, lhsThreadInfo == rhsThreadInfo, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsExpandType == rhsExpandType {
return true
} else {
return false
}
case let .recentlySearchedPeer(lhsPeer, lhsAssociatedPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder): case let .recentlySearchedPeer(lhsPeer, lhsAssociatedPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder):
if case let .recentlySearchedPeer(rhsPeer, rhsAssociatedPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 { if case let .recentlySearchedPeer(rhsPeer, rhsAssociatedPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 {
return true return true
@ -338,8 +348,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader, lhsKey, lhsResourceId, lhsSection, lhsAllPaused): case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsThreadInfo, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader, lhsKey, lhsResourceId, lhsSection, lhsAllPaused):
if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader, rhsKey, rhsResourceId, rhsSection, rhsAllPaused) = rhs { if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsThreadInfo, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader, rhsKey, rhsResourceId, rhsSection, rhsAllPaused) = rhs {
if lhsMessage.id != rhsMessage.id { if lhsMessage.id != rhsMessage.id {
return false return false
} }
@ -349,6 +359,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
if lhsPeer != rhsPeer { if lhsPeer != rhsPeer {
return false return false
} }
if lhsThreadInfo != rhsThreadInfo {
return false
}
if lhsPresentationData !== rhsPresentationData { if lhsPresentationData !== rhsPresentationData {
return false return false
} }
@ -403,15 +416,23 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
public static func <(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool { public static func <(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool {
switch lhs { switch lhs {
case let .topic(_, _, lhsIndex, _, _, _):
if case let .topic(_, _, rhsIndex, _, _, _) = rhs {
return lhsIndex <= rhsIndex
} else {
return true
}
case let .recentlySearchedPeer(_, _, _, lhsIndex, _, _, _, _): case let .recentlySearchedPeer(_, _, _, lhsIndex, _, _, _, _):
if case let .recentlySearchedPeer(_, _, _, rhsIndex, _, _, _, _) = rhs { if case .topic = rhs {
return false
} else if case let .recentlySearchedPeer(_, _, _, rhsIndex, _, _, _, _) = rhs {
return lhsIndex <= rhsIndex return lhsIndex <= rhsIndex
} else { } else {
return true return true
} }
case let .localPeer(_, _, _, lhsIndex, _, _, _, _, _): case let .localPeer(_, _, _, lhsIndex, _, _, _, _, _):
switch rhs { switch rhs {
case .recentlySearchedPeer: case .topic, .recentlySearchedPeer:
return false return false
case let .localPeer(_, _, _, rhsIndex, _, _, _, _, _): case let .localPeer(_, _, _, rhsIndex, _, _, _, _, _):
return lhsIndex <= rhsIndex return lhsIndex <= rhsIndex
@ -420,15 +441,15 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
} }
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _): case let .globalPeer(_, _, lhsIndex, _, _, _, _, _):
switch rhs { switch rhs {
case .recentlySearchedPeer, .localPeer: case .topic, .recentlySearchedPeer, .localPeer:
return false return false
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _): case let .globalPeer(_, _, rhsIndex, _, _, _, _, _):
return lhsIndex <= rhsIndex return lhsIndex <= rhsIndex
case .message, .addContact: case .message, .addContact:
return true return true
} }
case let .message(_, _, _, _, _, _, _, lhsKey, _, _, _): case let .message(_, _, _, _, _, _, _, _, lhsKey, _, _, _):
if case let .message(_, _, _, _, _, _, _, rhsKey, _, _, _) = rhs { if case let .message(_, _, _, _, _, _, _, _, rhsKey, _, _, _) = rhs {
return lhsKey < rhsKey return lhsKey < rhsKey
} else if case .addContact = rhs { } else if case .addContact = rhs {
return true return true
@ -440,8 +461,25 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
} }
} }
public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void) -> ListViewItem { public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void) -> ListViewItem {
switch self { switch self {
case let .topic(peer, threadInfo, _, theme, strings, expandType):
let actionTitle: String?
switch expandType {
case .none:
actionTitle = nil
case .expand:
actionTitle = strings.ChatList_Search_ShowMore
case .collapse:
actionTitle = strings.ChatList_Search_ShowLess
}
let header = ChatListSearchItemHeader(type: .topics, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : {
toggleExpandGlobalResults()
})
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .generalSearch, peer: .thread(peer: peer, title: threadInfo.info.title, icon: threadInfo.info.icon, color: 0), status: .none, badge: nil, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
interaction.peerSelected(peer, threadInfo.id, nil, nil)
}, contextAction: nil, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer)
case let .recentlySearchedPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder): case let .recentlySearchedPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder):
let primaryPeer: EnginePeer let primaryPeer: EnginePeer
var chatPeer: EnginePeer? var chatPeer: EnginePeer?
@ -664,7 +702,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture, location) peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture, location)
} }
}, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer) }, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer)
case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused): case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused):
let header: ChatListSearchItemHeader let header: ChatListSearchItemHeader
switch orderingKey { switch orderingKey {
case .downloading: case .downloading:
@ -694,7 +732,21 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
if isMedia { if isMedia {
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: true, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(id: peer.peerId), interaction: listInteraction, message: message._asMessage(), selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: key == .downloads ? header : nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: key != .downloads, isDownloadList: key == .downloads) 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: true, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(id: peer.peerId), interaction: listInteraction, message: message._asMessage(), selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: key == .downloads ? header : nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: key != .downloads, isDownloadList: key == .downloads)
} else { } else {
return ChatListItem(presentationData: presentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: message.index)), content: .peer(messages: [message], peer: peer, threadInfo: nil, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false, forumThreadTitle: nil), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) let index: EngineChatList.Item.Index
var chatThreadInfo: ChatListItemContent.ThreadInfo?
switch location {
case .chatList:
index = .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: message.index))
case .forum:
if let threadId = message.threadId, let threadInfo = threadInfo {
chatThreadInfo = ChatListItemContent.ThreadInfo(id: threadId, info: threadInfo)
index = .forum(timestamp: message.index.timestamp, threadId: threadId, namespace: message.index.id.namespace, id: message.index.id.id)
} else {
index = .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: message.index))
}
}
return ChatListItem(presentationData: presentationData, context: context, chatListLocation: location, filterData: nil, index: index, content: .peer(messages: [message], peer: peer, threadInfo: chatThreadInfo, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false, forumThreadTitle: nil), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
} }
case let .addContact(phoneNumber, theme, strings): case let .addContact(phoneNumber, theme, strings):
return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {
@ -742,12 +794,12 @@ private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [
return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates)
} }
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void) -> ChatListSearchContainerTransition { public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void) -> ChatListSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused), directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused), directionHint: nil) }
return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated)
} }
@ -851,7 +903,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private var presentationData: PresentationData private var presentationData: PresentationData
private let key: ChatListSearchPaneKey private let key: ChatListSearchPaneKey
private let tagMask: EngineMessage.Tags? private let tagMask: EngineMessage.Tags?
private let groupId: EngineChatList.Group? private let location: ChatListControllerLocation
private let navigationController: NavigationController? private let navigationController: NavigationController?
private let recentListNode: ListView private let recentListNode: ListView
@ -917,20 +969,29 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private var hiddenMediaDisposable: Disposable? private var hiddenMediaDisposable: Disposable?
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group?, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) { init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) {
self.context = context self.context = context
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
self.interaction = interaction self.interaction = interaction
self.key = key self.key = key
self.peersFilter = peersFilter self.location = location
self.groupId = groupId
self.navigationController = navigationController self.navigationController = navigationController
var peersFilter = peersFilter
if case .forum = location {
peersFilter.insert(.excludeRecent)
} else if case .chatList(.archive) = location {
peersFilter.insert(.excludeRecent)
}
self.peersFilter = peersFilter
let tagMask: EngineMessage.Tags? let tagMask: EngineMessage.Tags?
switch key { switch key {
case .chats: case .chats:
tagMask = nil tagMask = nil
case .topics:
tagMask = nil
case .media: case .media:
tagMask = .photoOrVideo tagMask = .photoOrVideo
case .downloads: case .downloads:
@ -1045,7 +1106,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let previousRecentlySearchedPeerOrder = Atomic<[EnginePeer.Id]>(value: []) let previousRecentlySearchedPeerOrder = Atomic<[EnginePeer.Id]>(value: [])
let fixedRecentlySearchedPeers: Signal<[RecentlySearchedPeer], NoError> let fixedRecentlySearchedPeers: Signal<[RecentlySearchedPeer], NoError>
if case .chats = key { if case .chats = key, case .chatList(.root) = location {
fixedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() fixedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers()
|> map { peers -> [RecentlySearchedPeer] in |> map { peers -> [RecentlySearchedPeer] in
var result: [RecentlySearchedPeer] = [] var result: [RecentlySearchedPeer] = []
@ -1122,7 +1183,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let foundItems = combineLatest(queue: .mainQueue(), searchQuery, searchOptions, downloadItems) let foundItems = combineLatest(queue: .mainQueue(), searchQuery, searchOptions, downloadItems)
|> mapToSignal { [weak self] query, options, downloadItems -> Signal<([ChatListSearchEntry], Bool)?, NoError> in |> mapToSignal { [weak self] query, options, downloadItems -> Signal<([ChatListSearchEntry], Bool)?, NoError> in
if query == nil && options == nil && key == .chats { if query == nil && options == nil && [.chats, .topics].contains(key) {
let _ = currentRemotePeers.swap(nil) let _ = currentRemotePeers.swap(nil)
return .single(nil) return .single(nil)
} }
@ -1186,7 +1247,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
resource = (resourceValue.id.stringRepresentation, size, entries.isEmpty) resource = (resourceValue.id.stringRepresentation, size, entries.isEmpty)
} }
entries.append(.message(message, peer, nil, presentationData, 1, nil, false, .downloading(item.priority), resource, .downloading, allPaused)) entries.append(.message(message, peer, nil, nil, presentationData, 1, nil, false, .downloading(item.priority), resource, .downloading, allPaused))
} }
for item in downloadItems.doneItems.sorted(by: { ChatListSearchEntry.MessageOrderingKey.downloaded(timestamp: $0.timestamp, index: $0.message.index) < ChatListSearchEntry.MessageOrderingKey.downloaded(timestamp: $1.timestamp, index: $1.message.index) }) { for item in downloadItems.doneItems.sorted(by: { ChatListSearchEntry.MessageOrderingKey.downloaded(timestamp: $0.timestamp, index: $0.message.index) < ChatListSearchEntry.MessageOrderingKey.downloaded(timestamp: $1.timestamp, index: $1.message.index) }) {
if !item.isSeen { if !item.isSeen {
@ -1214,7 +1275,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
} }
} }
entries.append(.message(message, peer, nil, presentationData, 1, selectionState?.contains(message.id), false, .downloaded(timestamp: item.timestamp, index: message.index), (item.resourceId, item.size, false), .recentlyDownloaded, false)) entries.append(.message(message, peer, nil, nil, presentationData, 1, selectionState?.contains(message.id), false, .downloaded(timestamp: item.timestamp, index: message.index), (item.resourceId, item.size, false), .recentlyDownloaded, false))
} }
return (entries.sorted(), false) return (entries.sorted(), false)
} }
@ -1328,7 +1389,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError> let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError>
let currentRemotePeersValue: ([FoundPeer], [FoundPeer]) = currentRemotePeers.with { $0 } ?? ([], []) let currentRemotePeersValue: ([FoundPeer], [FoundPeer]) = currentRemotePeers.with { $0 } ?? ([], [])
if let query = query, tagMask == nil { if let query = query, case .chats = key {
foundRemotePeers = ( foundRemotePeers = (
.single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true))
|> then( |> then(
@ -1340,22 +1401,26 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
} else { } else {
foundRemotePeers = .single(([], [], false)) foundRemotePeers = .single(([], [], false))
} }
let location: SearchMessagesLocation let searchLocation: SearchMessagesLocation
if let options = options { if let options = options {
if let (peerId, _, _) = options.peer { if case let .forum(peerId) = location {
location = .peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: options.date?.0, maxDate: options.date?.1) searchLocation = .peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: options.date?.0, maxDate: options.date?.1)
} else if let (peerId, _, _) = options.peer {
searchLocation = .peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: options.date?.0, maxDate: options.date?.1)
} else { } else {
if let groupId = groupId { if case let .chatList(groupId) = location, case .archive = groupId {
location = .group(groupId: groupId._asGroup(), tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1) searchLocation = .group(groupId: groupId._asGroup(), tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)
} else { } else {
location = .general(tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1) searchLocation = .general(tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)
} }
} }
} else { } else {
if let groupId = groupId { if case let .forum(peerId) = location {
location = .group(groupId: groupId._asGroup(), tags: tagMask, minDate: nil, maxDate: nil) searchLocation = .peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: nil, maxDate: nil)
} else if case let .chatList(groupId) = location, case .archive = groupId {
searchLocation = .group(groupId: groupId._asGroup(), tags: tagMask, minDate: nil, maxDate: nil)
} else { } else {
location = .general(tags: tagMask, minDate: nil, maxDate: nil) searchLocation = .general(tags: tagMask, minDate: nil, maxDate: nil)
} }
} }
@ -1371,7 +1436,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
addAppLogEvent(postbox: context.account.postbox, type: "search_global_query") addAppLogEvent(postbox: context.account.postbox, type: "search_global_query")
} }
let searchSignal = context.engine.messages.searchMessages(location: location, query: finalQuery, state: nil, limit: 50) let searchSignal = context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50)
|> map { result, updatedState -> ChatListSearchMessagesResult in |> map { result, updatedState -> ChatListSearchMessagesResult in
return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues(EnginePeerReadCounters.init), hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues(EnginePeerReadCounters.init), hasMore: !result.completed, totalCount: result.totalCount, state: updatedState)
} }
@ -1380,7 +1445,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|> mapToSignal { searchContext -> Signal<(([EngineMessage], [EnginePeer.Id: EnginePeerReadCounters], Int32), Bool), NoError> in |> mapToSignal { searchContext -> Signal<(([EngineMessage], [EnginePeer.Id: EnginePeerReadCounters], Int32), Bool), NoError> in
if let searchContext = searchContext, searchContext.result.hasMore { if let searchContext = searchContext, searchContext.result.hasMore {
if let _ = searchContext.loadMoreIndex { if let _ = searchContext.loadMoreIndex {
return context.engine.messages.searchMessages(location: location, query: finalQuery, state: searchContext.result.state, limit: 80) return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: searchContext.result.state, limit: 80)
|> map { result, updatedState -> ChatListSearchMessagesResult in |> map { result, updatedState -> ChatListSearchMessagesResult in
return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues(EnginePeerReadCounters.init), hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues(EnginePeerReadCounters.init), hasMore: !result.completed, totalCount: result.totalCount, state: updatedState)
} }
@ -1426,12 +1491,40 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
} }
}) })
return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get(), selectionPromise.get(), resolvedMessage, fixedRecentlySearchedPeers) let foundThreads: Signal<([Int64: EngineMessageHistoryThread.Info], [EngineChatList.Item]), NoError> = chatListViewForLocation(chatListLocation: location, location: .initial(count: 1000, filter: nil), account: context.account)
|> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers -> ([ChatListSearchEntry], Bool)? in |> map { view -> ([Int64: EngineMessageHistoryThread.Info], [EngineChatList.Item]) in
var itemsMap: [Int64: EngineMessageHistoryThread.Info] = [:]
var filteredItems: [EngineChatList.Item] = []
let queryTokens = stringIndexTokens(finalQuery, transliteration: .combined)
for item in view.list.items {
if case let .forum(_, index, _, _) = item.index, let threadInfo = item.threadInfo {
itemsMap[index] = threadInfo
}
if !finalQuery.isEmpty {
if let title = item.threadInfo?.title {
let tokens = stringIndexTokens(title, transliteration: .combined)
if matchStringIndexTokens(tokens, with: queryTokens) {
filteredItems.append(item)
}
}
}
}
return (itemsMap, filteredItems)
}
return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get(), selectionPromise.get(), resolvedMessage, fixedRecentlySearchedPeers, foundThreads)
|> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in
let isSearching = foundRemotePeers.2 || foundRemoteMessages.1 let isSearching = foundRemotePeers.2 || foundRemoteMessages.1
var entries: [ChatListSearchEntry] = [] var entries: [ChatListSearchEntry] = []
var index = 0 var index = 0
for thread in allAndFoundThreads.1 {
if let peer = thread.renderedPeer.peer, let threadInfo = thread.threadInfo, case let .forum(_, id, _, _) = thread.index {
entries.append(.topic(peer, ChatListItemContent.ThreadInfo(id: id, info: threadInfo), index, presentationData.theme, presentationData.strings, .none))
index += 1
}
}
var recentPeers = recentPeers var recentPeers = recentPeers
if query != nil { if query != nil {
recentPeers = [] recentPeers = []
@ -1623,7 +1716,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer)) peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
} }
} }
entries.append(.message(message, peer, nil, presentationData, 1, nil, true, .index(message.index), nil, .generic, false)) entries.append(.message(message, peer, nil, message.threadId.flatMap { allAndFoundThreads.0[$0] }, presentationData, 1, nil, true, .index(message.index), nil, .generic, false))
index += 1 index += 1
} }
@ -1646,12 +1739,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer)) peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
} }
} }
entries.append(.message(message, peer, foundRemoteMessages.0.1[message.id.peerId], presentationData, foundRemoteMessages.0.2, selectionState?.contains(message.id), headerId == firstHeaderId, .index(message.index), nil, .generic, false)) entries.append(.message(message, peer, foundRemoteMessages.0.1[message.id.peerId], message.threadId.flatMap { allAndFoundThreads.0[$0] }, presentationData, foundRemoteMessages.0.2, selectionState?.contains(message.id), headerId == firstHeaderId, .index(message.index), nil, .generic, false))
index += 1 index += 1
} }
} }
if tagMask == nil, !peersFilter.contains(.excludeRecent), isViablePhoneNumber(finalQuery) { if case .chats = key, !peersFilter.contains(.excludeRecent), isViablePhoneNumber(finalQuery) {
entries.append(.addContact(finalQuery, presentationData.theme, presentationData.strings)) entries.append(.addContact(finalQuery, presentationData.theme, presentationData.strings))
} }
@ -1727,10 +1820,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}, togglePeerSelected: { _ in }, togglePeerSelected: { _ in
}, togglePeersSelection: { _, _ in }, togglePeersSelection: { _, _ in
}, additionalCategorySelected: { _ in }, additionalCategorySelected: { _ in
}, messageSelected: { [weak self] peer, _, message, _ in }, messageSelected: { [weak self] peer, threadId, message, _ in
interaction.dismissInput() interaction.dismissInput()
if let strongSelf = self, let peer = message.peers[message.id.peerId] { if let strongSelf = self, let peer = message.peers[message.id.peerId] {
interaction.openMessage(EnginePeer(peer), message.id, strongSelf.key == .chats) interaction.openMessage(EnginePeer(peer), threadId, message.id, strongSelf.key == .chats)
} }
self?.listNode.clearHighlightAnimated(true) self?.listNode.clearHighlightAnimated(true)
}, groupSelected: { _ in }, groupSelected: { _ in
@ -1826,7 +1919,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
var fetchResourceId: (id: String, size: Int64, isFirstInList: Bool)? var fetchResourceId: (id: String, size: Int64, isFirstInList: Bool)?
for entry in currentEntries { for entry in currentEntries {
switch entry { switch entry {
case let .message(m, _, _, _, _, _, _, _, resource, _, _): case let .message(m, _, _, _, _, _, _, _, _, resource, _, _):
if m.id == message.id { if m.id == message.id {
fetchResourceId = resource fetchResourceId = resource
} }
@ -1907,7 +2000,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let animated = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil) let animated = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil)
let firstTime = previousEntries == nil let firstTime = previousEntries == nil
var transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, key: strongSelf.key, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture, location in var transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, location: location, key: strongSelf.key, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture, location in
interaction.peerContextAction?(message, node, rect, gesture, location) interaction.peerContextAction?(message, node, rect, gesture, location)
}, toggleExpandLocalResults: { }, toggleExpandLocalResults: {
guard let strongSelf = self else { guard let strongSelf = self else {
@ -1999,7 +2092,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
var messages: [EngineMessage] = [] var messages: [EngineMessage] = []
for entry in newEntries { for entry in newEntries {
if case let .message(message, _, _, _, _, _, _, _, _, _, _) = entry { if case let .message(message, _, _, _, _, _, _, _, _, _, _, _) = entry {
messages.append(message) messages.append(message)
} }
} }
@ -2051,7 +2144,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
recentItems = .single([]) recentItems = .single([])
} }
if tagMask == nil && !peersFilter.contains(.excludeRecent) { if case .chats = key, !peersFilter.contains(.excludeRecent) {
self.updatedRecentPeersDisposable.set(context.engine.peers.managedUpdatedRecentPeers().start()) self.updatedRecentPeersDisposable.set(context.engine.peers.managedUpdatedRecentPeers().start())
} }
@ -2664,7 +2757,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
self.enqueuedTransitions.remove(at: 0) self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions() var options = ListViewDeleteAndInsertOptions()
if isFirstTime && self.key == .chats { if isFirstTime && [.chats, .topics].contains(self.key) {
options.insert(.PreferSynchronousDrawing) options.insert(.PreferSynchronousDrawing)
options.insert(.PreferSynchronousResourceLoading) options.insert(.PreferSynchronousResourceLoading)
} else if transition.animated { } else if transition.animated {
@ -2737,7 +2830,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
strongSelf.emptyResultsAnimationNode.visibility = emptyResults strongSelf.emptyResultsAnimationNode.visibility = emptyResults
} }
var displayPlaceholder = transition.isLoading && (strongSelf.key != .chats || (strongSelf.currentEntries?.isEmpty ?? true)) var displayPlaceholder = transition.isLoading && (![.chats, .topics].contains(strongSelf.key) || (strongSelf.currentEntries?.isEmpty ?? true))
if strongSelf.key == .downloads { if strongSelf.key == .downloads {
displayPlaceholder = false displayPlaceholder = false
} }
@ -2959,7 +3052,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
let items = (0 ..< 2).compactMap { _ -> ListViewItem? in let items = (0 ..< 2).compactMap { _ -> ListViewItem? in
switch key { switch key {
case .chats, .downloads: case .chats, .topics, .downloads:
let message = EngineMessage( let message = EngineMessage(
stableId: 0, stableId: 0,
stableVersion: 0, stableVersion: 0,

View File

@ -702,7 +702,7 @@ final class ChatListSearchMediaNode: ASDisplayNode, UIScrollViewDelegate {
var index: UInt32 = 0 var index: UInt32 = 0
if let entries = entries { if let entries = entries {
for entry in entries { for entry in entries {
if case let .message(message, _, _, _, _, _, _, _, _, _, _) = entry { if case let .message(message, _, _, _, _, _, _, _, _, _, _, _) = entry {
self.mediaItems.append(VisualMediaItem(message: message._asMessage(), index: nil)) self.mediaItems.append(VisualMediaItem(message: message._asMessage(), index: nil))
} }
index += 1 index += 1

View File

@ -49,6 +49,7 @@ final class ChatListSearchPaneWrapper {
public enum ChatListSearchPaneKey { public enum ChatListSearchPaneKey {
case chats case chats
case topics
case media case media
case downloads case downloads
case links case links
@ -62,6 +63,8 @@ extension ChatListSearchPaneKey {
switch self { switch self {
case .chats: case .chats:
return .chats return .chats
case .topics:
return .topics
case .media: case .media:
return .media return .media
case .downloads: case .downloads:
@ -78,9 +81,15 @@ extension ChatListSearchPaneKey {
} }
} }
func defaultAvailableSearchPanes(hasDownloads: Bool) -> [ChatListSearchPaneKey] { func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool) -> [ChatListSearchPaneKey] {
var result: [ChatListSearchPaneKey] = [.chats, .media, .downloads, .links, .files, .music, .voice] var result: [ChatListSearchPaneKey] = []
if isForum {
result.append(.topics)
} else {
result.append(.chats)
}
result.append(contentsOf: [.media, .downloads, .links, .files, .music, .voice])
if !hasDownloads { if !hasDownloads {
result.removeAll(where: { $0 == .downloads }) result.removeAll(where: { $0 == .downloads })
} }
@ -110,13 +119,13 @@ private final class ChatListSearchPendingPane {
interaction: ChatListSearchInteraction, interaction: ChatListSearchInteraction,
navigationController: NavigationController?, navigationController: NavigationController?,
peersFilter: ChatListNodePeersFilter, peersFilter: ChatListNodePeersFilter,
groupId: EngineChatList.Group, location: ChatListControllerLocation,
searchQuery: Signal<String?, NoError>, searchQuery: Signal<String?, NoError>,
searchOptions: Signal<ChatListSearchOptions?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>,
key: ChatListSearchPaneKey, key: ChatListSearchPaneKey,
hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void
) { ) {
let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: key == .chats ? peersFilter : [], groupId: groupId, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: key == .chats ? peersFilter : [], location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController)
self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode) self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode)
self.disposable = (paneNode.isReady self.disposable = (paneNode.isReady
@ -138,7 +147,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
private let animationRenderer: MultiAnimationRenderer private let animationRenderer: MultiAnimationRenderer
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
private let peersFilter: ChatListNodePeersFilter private let peersFilter: ChatListNodePeersFilter
private let groupId: EngineChatList.Group private let location: ChatListControllerLocation
private let searchQuery: Signal<String?, NoError> private let searchQuery: Signal<String?, NoError>
private let searchOptions: Signal<ChatListSearchOptions?, NoError> private let searchOptions: Signal<ChatListSearchOptions?, NoError>
private let navigationController: NavigationController? private let navigationController: NavigationController?
@ -172,13 +181,13 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
private var currentAvailablePanes: [ChatListSearchPaneKey]? private var currentAvailablePanes: [ChatListSearchPaneKey]?
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peersFilter: ChatListNodePeersFilter, groupId: EngineChatList.Group, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) { init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peersFilter: ChatListNodePeersFilter, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) {
self.context = context self.context = context
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
self.updatedPresentationData = updatedPresentationData self.updatedPresentationData = updatedPresentationData
self.peersFilter = peersFilter self.peersFilter = peersFilter
self.groupId = groupId self.location = location
self.searchQuery = searchQuery self.searchQuery = searchQuery
self.searchOptions = searchOptions self.searchOptions = searchOptions
self.navigationController = navigationController self.navigationController = navigationController
@ -408,7 +417,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
interaction: self.interaction!, interaction: self.interaction!,
navigationController: self.navigationController, navigationController: self.navigationController,
peersFilter: self.peersFilter, peersFilter: self.peersFilter,
groupId: self.groupId, location: self.location,
searchQuery: self.searchQuery, searchQuery: self.searchQuery,
searchOptions: self.searchOptions, searchOptions: self.searchOptions,
key: key, key: key,

View File

@ -221,6 +221,8 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
} }
case .deviceContact: case .deviceContact:
break break
case .thread:
break
} }
} }
} }

View File

@ -97,11 +97,30 @@ public struct ContactsPeerItemAction {
} }
public enum ContactsPeerItemPeer: Equatable { public enum ContactsPeerItemPeer: Equatable {
case thread(peer: EnginePeer, title: String, icon: Int64?, color: Int32)
case peer(peer: EnginePeer?, chatPeer: EnginePeer?) case peer(peer: EnginePeer?, chatPeer: EnginePeer?)
case deviceContact(stableId: DeviceContactStableId, contact: DeviceContactBasicData) case deviceContact(stableId: DeviceContactStableId, contact: DeviceContactBasicData)
public static func ==(lhs: ContactsPeerItemPeer, rhs: ContactsPeerItemPeer) -> Bool { public static func ==(lhs: ContactsPeerItemPeer, rhs: ContactsPeerItemPeer) -> Bool {
switch lhs { switch lhs {
case let .thread(lhsPeer, lhsTitle, lhsIcon, lhsColor):
if case let .thread(rhsPeer, rhsTitle, rhsIcon, rhsColor) = rhs {
if lhsPeer != rhsPeer {
return false
}
if lhsTitle != rhsTitle {
return false
}
if lhsIcon != rhsIcon {
return false
}
if lhsColor != rhsColor {
return false
}
return true
} else {
return false
}
case let .peer(lhsPeer, lhsChatPeer): case let .peer(lhsPeer, lhsChatPeer):
if case let .peer(rhsPeer, rhsChatPeer) = rhs { if case let .peer(rhsPeer, rhsChatPeer) = rhs {
if lhsPeer != rhsPeer { if lhsPeer != rhsPeer {
@ -224,6 +243,8 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
if let index = index { if let index = index {
var letter: String = "#" var letter: String = "#"
switch peer { switch peer {
case let .thread(_, title, _, _):
letter = String(title.prefix(1)).uppercased()
case let .peer(peer, _): case let .peer(peer, _):
if case let .user(user) = peer { if case let .user(user) = peer {
switch index { switch index {
@ -368,6 +389,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
private let offsetContainerNode: ASDisplayNode private let offsetContainerNode: ASDisplayNode
private let avatarNode: AvatarNode private let avatarNode: AvatarNode
private var avatarIconView: ComponentHostView<Empty>?
private var avatarIconComponent: EmojiStatusComponent?
private let titleNode: TextNode private let titleNode: TextNode
private var credibilityIconView: ComponentHostView<Empty>? private var credibilityIconView: ComponentHostView<Empty>?
private var credibilityIconComponent: EmojiStatusComponent? private var credibilityIconComponent: EmojiStatusComponent?
@ -389,6 +412,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
return chatPeer ?? peer return chatPeer ?? peer
case .deviceContact: case .deviceContact:
return nil return nil
case .thread:
return nil
} }
} else { } else {
return nil return nil
@ -426,6 +451,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
containerSize: credibilityIconView.bounds.size containerSize: credibilityIconView.bounds.size
) )
} }
if let avatarIconView = self.avatarIconView, let avatarIconComponent = self.avatarIconComponent {
let _ = avatarIconView.update(
transition: .immediate,
component: AnyComponent(avatarIconComponent.withVisibleForAnimations(self.visibilityStatus)),
environment: {},
containerSize: avatarIconView.bounds.size
)
}
} }
} }
} }
@ -612,6 +645,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
var leftInset: CGFloat = 65.0 + params.leftInset var leftInset: CGFloat = 65.0 + params.leftInset
var rightInset: CGFloat = 10.0 + params.rightInset var rightInset: CGFloat = 10.0 + params.rightInset
if case .thread = item.peer {
leftInset -= 13.0
}
let updatedSelectionNode: CheckNode? let updatedSelectionNode: CheckNode?
var isSelected = false var isSelected = false
switch item.selection { switch item.selection {
@ -657,6 +694,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
} }
case .deviceContact: case .deviceContact:
break break
case .thread:
break
} }
var arrowButtonImage: UIImage? var arrowButtonImage: UIImage?
@ -698,6 +737,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
var userPresence: EnginePeer.Presence? var userPresence: EnginePeer.Presence?
switch item.peer { switch item.peer {
case let .thread(_, title, _, _):
titleAttributedString = NSAttributedString(string: title, font: titleBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
case let .peer(peer, chatPeer): case let .peer(peer, chatPeer):
if let peer = peer { if let peer = peer {
let textColor: UIColor let textColor: UIColor
@ -943,6 +984,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
letters = [" "] letters = [" "]
} }
strongSelf.avatarNode.setCustomLetters(letters) strongSelf.avatarNode.setCustomLetters(letters)
case .thread:
break
} }
let transition: ContainedViewLayoutTransition let transition: ContainedViewLayoutTransition
@ -982,18 +1025,63 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
hasTopCorners = true hasTopCorners = true
strongSelf.topSeparatorNode.isHidden = hasCorners strongSelf.topSeparatorNode.isHidden = hasCorners
} }
} switch neighbors.bottom {
case .sameSection(false):
switch neighbors.bottom { strongSelf.separatorNode.isHidden = false
case .sameSection(false): default:
strongSelf.separatorNode.isHidden = false hasBottomCorners = true
default: strongSelf.separatorNode.isHidden = hasCorners
hasBottomCorners = true }
strongSelf.separatorNode.isHidden = hasCorners
} }
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))) transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)))
if case let .thread(_, title, icon, color) = item.peer {
let animationCache = item.context.animationCache
let animationRenderer = item.context.animationRenderer
let avatarIconView: ComponentHostView<Empty>
if let current = strongSelf.avatarIconView {
avatarIconView = current
} else {
avatarIconView = ComponentHostView<Empty>()
strongSelf.avatarIconView = avatarIconView
strongSelf.offsetContainerNode.view.addSubview(avatarIconView)
}
let avatarIconContent: EmojiStatusComponent.Content
if let fileId = icon, fileId != 0 {
avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: nil, loopMode: .forever)
} else {
avatarIconContent = .topic(title: String(title.prefix(1)), colorIndex: Int(clamping: abs(color)), size: CGSize(width: 32.0, height: 32.0))
}
let avatarIconComponent = EmojiStatusComponent(
context: item.context,
animationCache: animationCache,
animationRenderer: animationRenderer,
content: avatarIconContent,
isVisibleForAnimations: strongSelf.visibilityStatus,
action: nil
)
strongSelf.avatarIconComponent = avatarIconComponent
let iconSize = avatarIconView.update(
transition: .immediate,
component: AnyComponent(avatarIconComponent),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
transition.updateFrame(view: avatarIconView, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 43.0, y: floor((nodeLayout.contentSize.height - iconSize.height) / 2.0)), size: iconSize))
strongSelf.avatarNode.isHidden = true
} else if let avatarIconView = strongSelf.avatarIconView {
strongSelf.avatarIconView = nil
avatarIconView.removeFromSuperview()
strongSelf.avatarNode.isHidden = false
}
let _ = titleApply() let _ = titleApply()
let titleFrame = titleFrame.offsetBy(dx: revealOffset, dy: 0.0) let titleFrame = titleFrame.offsetBy(dx: revealOffset, dy: 0.0)
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame) transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
@ -1269,6 +1357,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
} }
case .deviceContact: case .deviceContact:
break break
case .thread:
break
} }
} }
} }
@ -1282,6 +1372,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
} }
case .deviceContact: case .deviceContact:
break break
case .thread:
break
} }
} }
} }
@ -1296,6 +1388,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
} }
case .deviceContact: case .deviceContact:
break break
case .thread:
break
} }
} else { } else {
item.options[Int(option.key)].action() item.options[Int(option.key)].action()

View File

@ -54,7 +54,7 @@ public final class HashtagSearchController: TelegramBaseController {
|> map { result, presentationData in |> map { result, presentationData in
let result = result.0 let result = result.0
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap(EnginePeerReadCounters.init), chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic, false) }) return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap(EnginePeerReadCounters.init), nil, chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic, false) })
} }
let interaction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { let interaction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {
}, peerSelected: { _, _, _, _ in }, peerSelected: { _, _, _, _ in
@ -106,7 +106,7 @@ public final class HashtagSearchController: TelegramBaseController {
}) })
let firstTime = previousEntries == nil let firstTime = previousEntries == nil
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], key: .chats, tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: { let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], location: .chatList(groupId: .root), key: .chats, tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: {
}, toggleExpandGlobalResults: { }, toggleExpandGlobalResults: {
}, searchPeer: { _ in }, searchPeer: { _ in
}, searchQuery: "", searchOptions: nil, messageContextAction: nil, openClearRecentlyDownloaded: {}, toggleAllPaused: {}) }, searchQuery: "", searchOptions: nil, messageContextAction: nil, openClearRecentlyDownloaded: {}, toggleAllPaused: {})

View File

@ -14,9 +14,12 @@ swift_library(
"//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display", "//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramCore:TelegramCore",
"//submodules/ActivityIndicator:ActivityIndicator", "//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/AppBundle:AppBundle", "//submodules/AppBundle:AppBundle",
"//submodules/ComponentFlow:ComponentFlow", "//submodules/ComponentFlow:ComponentFlow",
"//submodules/AvatarNode:AvatarNode",
"//submodules/AccountContext:AccountContext",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -3,9 +3,12 @@ import UIKit
import SwiftSignalKit import SwiftSignalKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import TelegramCore
import TelegramPresentationData import TelegramPresentationData
import ActivityIndicator import ActivityIndicator
import AppBundle import AppBundle
import AvatarNode
import AccountContext
private func generateLoupeIcon(color: UIColor) -> UIImage? { private func generateLoupeIcon(color: UIColor) -> UIImage? {
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: color) return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: color)
@ -42,14 +45,16 @@ public struct SearchBarToken {
public let id: AnyHashable public let id: AnyHashable
public let icon: UIImage? public let icon: UIImage?
public let iconOffset: CGFloat? public let iconOffset: CGFloat?
public let peer: (EnginePeer, AccountContext, PresentationTheme)?
public let title: String public let title: String
public let style: Style? public let style: Style?
public let permanent: Bool public let permanent: Bool
public init(id: AnyHashable, icon: UIImage?, iconOffset: CGFloat? = 0.0, title: String, style: Style? = nil, permanent: Bool) { public init(id: AnyHashable, icon: UIImage?, iconOffset: CGFloat? = 0.0, peer: (EnginePeer, AccountContext, PresentationTheme)? = nil, title: String, style: Style? = nil, permanent: Bool) {
self.id = id self.id = id
self.icon = icon self.icon = icon
self.iconOffset = iconOffset self.iconOffset = iconOffset
self.peer = peer
self.title = title self.title = title
self.style = style self.style = style
self.permanent = permanent self.permanent = permanent
@ -63,6 +68,7 @@ private final class TokenNode: ASDisplayNode {
let iconNode: ASImageNode let iconNode: ASImageNode
let titleNode: ASTextNode let titleNode: ASTextNode
let backgroundNode: ASImageNode let backgroundNode: ASImageNode
let avatarNode: AvatarNode?
var isSelected: Bool = false var isSelected: Bool = false
var isCollapsed: Bool = false var isCollapsed: Bool = false
@ -85,22 +91,34 @@ private final class TokenNode: ASDisplayNode {
self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displayWithoutProcessing = true
if let _ = token.peer {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0))
} else {
self.avatarNode = nil
}
super.init() super.init()
self.clipsToBounds = true self.clipsToBounds = true
self.addSubnode(self.containerNode) self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundNode)
let backgroundColor = token.style?.backgroundColor ?? theme.inputIcon if let avatarNode = self.avatarNode, let (peer, context, theme) = token.peer {
let strokeColor = token.style?.strokeColor ?? backgroundColor avatarNode.setPeer(context: context, theme: theme, peer: peer, clipStyle: .roundedRect, displayDimensions: CGSize(width: 24.0, height: 24.0))
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 8.0, color: backgroundColor, strokeColor: strokeColor, strokeWidth: UIScreenPixel, backgroundColor: nil) self.containerNode.addSubnode(avatarNode)
} else {
let foregroundColor = token.style?.foregroundColor ?? .white self.containerNode.addSubnode(self.backgroundNode)
self.iconNode.image = generateTintedImage(image: token.icon, color: foregroundColor)
self.containerNode.addSubnode(self.iconNode) let backgroundColor = token.style?.backgroundColor ?? theme.inputIcon
let strokeColor = token.style?.strokeColor ?? backgroundColor
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(17.0), textColor: foregroundColor) self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 8.0, color: backgroundColor, strokeColor: strokeColor, strokeWidth: UIScreenPixel, backgroundColor: nil)
self.containerNode.addSubnode(self.titleNode)
let foregroundColor = token.style?.foregroundColor ?? .white
self.iconNode.image = generateTintedImage(image: token.icon, color: foregroundColor)
self.containerNode.addSubnode(self.iconNode)
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(17.0), textColor: foregroundColor)
self.containerNode.addSubnode(self.titleNode)
}
} }
override func didLoad() { override func didLoad() {
@ -110,7 +128,6 @@ private final class TokenNode: ASDisplayNode {
} }
@objc private func tapGesture() { @objc private func tapGesture() {
self.tapped?() self.tapped?()
} }
@ -119,6 +136,11 @@ private final class TokenNode: ASDisplayNode {
self.containerNode.layer.animateFrame(from: CGRect(origin: targetFrame.origin, size: CGSize(width: 1.0, height: targetFrame.height)), to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) self.containerNode.layer.animateFrame(from: CGRect(origin: targetFrame.origin, size: CGSize(width: 1.0, height: targetFrame.height)), to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.backgroundNode.layer.animateFrame(from: CGRect(origin: targetFrame.origin, size: CGSize(width: 1.0, height: targetFrame.height)), to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) self.backgroundNode.layer.animateFrame(from: CGRect(origin: targetFrame.origin, size: CGSize(width: 1.0, height: targetFrame.height)), to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
if let avatarNode = self.avatarNode {
avatarNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
self.titleNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) self.titleNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
@ -175,11 +197,17 @@ private final class TokenNode: ASDisplayNode {
width += iconSize.width + 7.0 width += iconSize.width + 7.0
} }
let size = CGSize(width: self.isCollapsed ? height : width, height: height) let size: CGSize
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) if let avatarNode = self.avatarNode {
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) size = CGSize(width: height, height: height)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)) transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: avatarNode, frame: CGRect(origin: CGPoint(), size: size))
} else {
size = CGSize(width: self.isCollapsed ? height : width, height: height)
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize))
}
return size return size
} }
} }

View File

@ -81,7 +81,9 @@ public final class SearchDisplayController {
strongSelf.searchBar.tokens = tokens strongSelf.searchBar.tokens = tokens
strongSelf.searchBar.text = query strongSelf.searchBar.text = query
if previousTokens.count < tokens.count && !isFirstTime { if previousTokens.count < tokens.count && !isFirstTime {
strongSelf.searchBar.selectLastToken() if let lastToken = tokens.last, !lastToken.permanent {
strongSelf.searchBar.selectLastToken()
}
} }
isFirstTime = false isFirstTime = false
} }

View File

@ -146,6 +146,11 @@ public final class SelectablePeerNode: ASDisplayNode {
let defaultColor: UIColor = peer.peerId.namespace == Namespaces.Peer.SecretChat ? self.theme.secretTextColor : self.theme.textColor let defaultColor: UIColor = peer.peerId.namespace == Namespaces.Peer.SecretChat ? self.theme.secretTextColor : self.theme.textColor
var isForum = false
if let peer = peer.chatMainPeer, case let .channel(channel) = peer, channel.flags.contains(.isForum) {
isForum = true
}
let text: String let text: String
var overrideImage: AvatarNodeImageOverride? var overrideImage: AvatarNodeImageOverride?
if peer.peerId == context.account.peerId { if peer.peerId == context.account.peerId {
@ -162,7 +167,7 @@ public final class SelectablePeerNode: ASDisplayNode {
} }
self.textNode.maximumNumberOfLines = numberOfLines self.textNode.maximumNumberOfLines = numberOfLines
self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : defaultColor, paragraphAlignment: .center) self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : defaultColor, paragraphAlignment: .center)
self.avatarNode.setPeer(context: context, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, synchronousLoad: synchronousLoad) self.avatarNode.setPeer(context: context, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: isForum ? .roundedRect : .round, synchronousLoad: synchronousLoad)
let onlineLayout = self.onlineNode.asyncLayout() let onlineLayout = self.onlineNode.asyncLayout()
let (onlineSize, onlineApply) = onlineLayout(online, false) let (onlineSize, onlineApply) = onlineLayout(online, false)
@ -182,16 +187,29 @@ public final class SelectablePeerNode: ASDisplayNode {
self.textNode.attributedText = NSAttributedString(string: attributedText.string, font: textFont, textColor: selected ? self.theme.selectedTextColor : (self.peer?.peerId.namespace == Namespaces.Peer.SecretChat ? self.theme.secretTextColor : self.theme.textColor), paragraphAlignment: .center) self.textNode.attributedText = NSAttributedString(string: attributedText.string, font: textFont, textColor: selected ? self.theme.selectedTextColor : (self.peer?.peerId.namespace == Namespaces.Peer.SecretChat ? self.theme.secretTextColor : self.theme.textColor), paragraphAlignment: .center)
} }
var isForum = false
if let peer = self.peer?.chatMainPeer, case let .channel(channel) = peer, channel.flags.contains(.isForum) {
isForum = true
}
if selected { if selected {
self.avatarNode.transform = CATransform3DMakeScale(0.866666, 0.866666, 1.0) self.avatarNode.transform = CATransform3DMakeScale(0.866666, 0.866666, 1.0)
self.avatarSelectionNode.alpha = 1.0 self.avatarSelectionNode.alpha = 1.0
self.avatarSelectionNode.image = generateImage(CGSize(width: 60.0 + 4.0, height: 60.0 + 4.0), rotatedContext: { size, context in self.avatarSelectionNode.image = generateImage(CGSize(width: 60.0 + 4.0, height: 60.0 + 4.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(self.theme.selectedTextColor.cgColor) let bounds = CGRect(origin: .zero, size: size)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) if isForum {
context.setBlendMode(.copy) context.setStrokeColor(self.theme.selectedTextColor.cgColor)
context.setFillColor(UIColor.clear.cgColor) context.setLineWidth(2.0)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: size.width - 4.0, height: size.height - 4.0))) context.addPath(UIBezierPath(roundedRect: bounds.insetBy(dx: 1.0, dy: 1.0), cornerRadius: floorToScreenPixels(bounds.size.width * 0.26)).cgPath)
context.strokePath()
} else {
context.setFillColor(self.theme.selectedTextColor.cgColor)
context.fillEllipse(in: bounds)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: bounds.insetBy(dx: 2.0, dy: 2.0))
}
}) })
if animated { if animated {
self.avatarNode.layer.animateScale(from: 1.0, to: 0.866666, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) self.avatarNode.layer.animateScale(from: 1.0, to: 0.866666, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring)

View File

@ -64,7 +64,11 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
if isVoice { if isVoice {
refinedTag = .voiceOrInstantVideo refinedTag = .voiceOrInstantVideo
} else { } else {
refinedTag = .music if file.isInstantVideo {
refinedTag = .voiceOrInstantVideo
} else {
refinedTag = .music
}
} }
break inner break inner
case .Sticker: case .Sticker:

View File

@ -64,7 +64,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
var requestOpenPeer: ((Peer) -> Void)? var requestOpenPeer: ((Peer) -> Void)?
var requestOpenDisabledPeer: ((Peer) -> Void)? var requestOpenDisabledPeer: ((Peer) -> Void)?
var requestOpenPeerFromSearch: ((Peer) -> Void)? var requestOpenPeerFromSearch: ((Peer) -> Void)?
var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)? var requestOpenMessageFromSearch: ((Peer, Int64?, MessageId) -> Void)?
var requestSend: (([Peer], [PeerId: Peer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?) -> Void)? var requestSend: (([Peer], [PeerId: Peer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?) -> Void)?
private var presentationData: PresentationData { private var presentationData: PresentationData {
@ -596,7 +596,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
animationRenderer: self.animationRenderer, animationRenderer: self.animationRenderer,
updatedPresentationData: self.updatedPresentationData, updatedPresentationData: self.updatedPresentationData,
filter: self.filter, filter: self.filter,
groupId: EngineChatList.Group(.root), location: .chatList(groupId: EngineChatList.Group(.root)),
displaySearchFilters: false, displaySearchFilters: false,
hasDownloads: false, hasDownloads: false,
openPeer: { [weak self] peer, chatPeer, _ in openPeer: { [weak self] peer, chatPeer, _ in
@ -650,9 +650,9 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}, },
openRecentPeerOptions: { _ in openRecentPeerOptions: { _ in
}, },
openMessage: { [weak self] peer, messageId, _ in openMessage: { [weak self] peer, threadId, messageId, _ in
if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch { if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch {
requestOpenMessageFromSearch(peer._asPeer(), messageId) requestOpenMessageFromSearch(peer._asPeer(), threadId, messageId)
} }
}, },
addContact: nil, addContact: nil,