diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index d1a0b70229..3c596ca0b3 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1695,25 +1695,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } activate() - updatedSearchOptionsImpl = { [weak self, weak filterContainerNode] options, hasDate in + var currentHasSuggestions = true + updatedSearchOptionsImpl = { [weak self, weak filterContainerNode] options, hasSuggestions in guard let strongSelf = self, let strongFilterContainerNode = filterContainerNode else { return } - var node: ASDisplayNode? - if let options = options, options.messageTags != nil && !hasDate { - } else { - node = strongFilterContainerNode - } + if currentHasSuggestions != hasSuggestions { + currentHasSuggestions = hasSuggestions - strongSelf.navigationBar?.setSecondaryContentNode(node, animated: false) - if let parentController = strongSelf.parent as? TabBarController { - parentController.navigationBar?.setSecondaryContentNode(node, animated: true) - } - - let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - if let layout = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, transition: transition) - (strongSelf.parent as? TabBarController)?.updateLayout(transition: transition) + var node: ASDisplayNode? + if let options = options, options.messageTags != nil && !hasSuggestions { + } else { + node = strongFilterContainerNode + } + + strongSelf.navigationBar?.setSecondaryContentNode(node, animated: false) + if let parentController = strongSelf.parent as? TabBarController { + parentController.navigationBar?.setSecondaryContentNode(node, animated: true) + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, transition: transition) + (strongSelf.parent as? TabBarController)?.updateLayout(transition: transition) + } } } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index f34c4ed06a..ccabe4e15a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -552,15 +552,19 @@ public struct ChatListSearchContainerTransition { public let updates: [ListViewUpdateItem] public let displayingResults: Bool public let isEmpty: Bool + public let isLoading: Bool public let query: String + public let animated: Bool - public init(deletions: [ListViewDeleteItem], insertions: [ListViewInsertItem], updates: [ListViewUpdateItem], displayingResults: Bool, isEmpty: Bool, query: String) { + public init(deletions: [ListViewDeleteItem], insertions: [ListViewInsertItem], updates: [ListViewUpdateItem], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, query: String, animated: Bool) { self.deletions = deletions self.insertions = insertions self.updates = updates self.displayingResults = displayingResults self.isEmpty = isEmpty + self.isLoading = isLoading self.query = query + self.animated = animated } } @@ -574,14 +578,14 @@ private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) } -public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, searchQuery: String, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (Peer) -> Void, searchResults: [Message], searchOptions: ChatListSearchOptions?, messageContextAction: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)?) -> ChatListSearchContainerTransition { +public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, searchQuery: String, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (Peer) -> Void, searchResults: [Message], searchOptions: ChatListSearchOptions?, messageContextAction: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)?) -> ChatListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) 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, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchResults: searchResults, searchOptions: searchOptions, messageContextAction: messageContextAction), 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, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchResults: searchResults, searchOptions: searchOptions, messageContextAction: messageContextAction), directionHint: nil) } - return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, query: searchQuery) + return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) } private struct ChatListSearchContainerNodeState: Equatable { @@ -653,26 +657,25 @@ public enum ChatListSearchContextActionSource { } public struct ChatListSearchOptions { - let peerId: PeerId? - let peerName: String? + let peer: (PeerId, String)? let minDate: Int32? let maxDate: Int32? let messageTags: MessageTags? - func withUpdatedPeerId(_ peerId: PeerId?, peerName: String?) -> ChatListSearchOptions { - return ChatListSearchOptions(peerId: peerId, peerName: peerName, minDate: self.minDate, maxDate: self.maxDate, messageTags: self.messageTags) + func withUpdatedPeer(_ peerIdAndName: (PeerId, String)?) -> ChatListSearchOptions { + return ChatListSearchOptions(peer: peerIdAndName, minDate: self.minDate, maxDate: self.maxDate, messageTags: self.messageTags) } func withUpdatedMinDate(_ minDate: Int32?) -> ChatListSearchOptions { - return ChatListSearchOptions(peerId: self.peerId, peerName: self.peerName, minDate: minDate, maxDate: self.maxDate, messageTags: self.messageTags) + return ChatListSearchOptions(peer: self.peer, minDate: minDate, maxDate: self.maxDate, messageTags: self.messageTags) } func withUpdatedMaxDate(_ maxDate: Int32?) -> ChatListSearchOptions { - return ChatListSearchOptions(peerId: self.peerId, peerName: self.peerName, minDate: self.minDate, maxDate: maxDate, messageTags: self.messageTags) + return ChatListSearchOptions(peer: self.peer, minDate: self.minDate, maxDate: maxDate, messageTags: self.messageTags) } func withUpdatedMessageTags(_ messageTags: MessageTags?) -> ChatListSearchOptions { - return ChatListSearchOptions(peerId: self.peerId, peerName: self.peerName, minDate: self.minDate, maxDate: self.maxDate, messageTags: messageTags) + return ChatListSearchOptions(peer: self.peer, minDate: self.minDate, maxDate: self.maxDate, messageTags: messageTags) } } @@ -686,6 +689,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let filterContainerNode: ChatListSearchFiltersContainerNode private var selectionPanelNode: ChatListSearchMessageSelectionPanelNode? private let recentListNode: ListView + private let loadingNode: ASImageNode private let listNode: ListView private let mediaNode: ChatListSearchMediaNode private let dimNode: ASDisplayNode @@ -770,6 +774,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo }, toggleMessageSelection: { messageId, selected in toggleMessageSelectionImpl?(messageId, selected) }) + + self.loadingNode = ASImageNode() self.listNode = ListView() self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor @@ -781,11 +787,13 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.mediaAccessoryPanelContainer.clipsToBounds = true self.emptyResultsTitleNode = ImmediateTextNode() + self.emptyResultsTitleNode.displaysAsynchronously = false self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) self.emptyResultsTitleNode.textAlignment = .center self.emptyResultsTitleNode.isHidden = true self.emptyResultsTextNode = ImmediateTextNode() + self.emptyResultsTextNode.displaysAsynchronously = false self.emptyResultsTextNode.maximumNumberOfLines = 0 self.emptyResultsTextNode.textAlignment = .center self.emptyResultsTextNode.isHidden = true @@ -807,6 +815,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.addSubnode(self.dimNode) self.addSubnode(self.recentListNode) self.addSubnode(self.listNode) + self.addSubnode(self.loadingNode) self.addSubnode(self.mediaNode) self.addSubnode(self.mediaAccessoryPanelContainer) @@ -901,7 +910,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } let location: SearchMessagesLocation if let options = options { - if let peerId = options.peerId { + if let (peerId, _) = options.peer { location = .peer(peerId: peerId, fromId: nil, tags: options.messageTags, topMsgId: nil, minDate: options.minDate, maxDate: options.maxDate) } else { @@ -930,7 +939,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let loadMore = searchContext.get() |> mapToSignal { searchContext -> Signal<(([Message], [PeerId: CombinedPeerReadState], Int32), Bool), NoError> in - if let searchContext = searchContext { + if let searchContext = searchContext, searchContext.result.hasMore { if let _ = searchContext.loadMoreIndex { return searchMessages(account: context.account, location: location, query: finalQuery, state: searchContext.result.state, limit: 80) |> map { result, updatedState -> ChatListSearchMessagesResult in @@ -1061,65 +1070,62 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo globalExpandType = .none } - if options?.messageTags != nil || options?.maxDate != nil || options?.peerId != nil { - } else { - let lowercasedQuery = finalQuery.lowercased() - if lowercasedQuery.count > 1 && presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) { - if !existingPeerIds.contains(accountPeer.id), filteredPeer(accountPeer, accountPeer) { - existingPeerIds.insert(accountPeer.id) - entries.append(.localPeer(accountPeer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) - index += 1 - } + let lowercasedQuery = finalQuery.lowercased() + if lowercasedQuery.count > 1 && (presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery)) { + if !existingPeerIds.contains(accountPeer.id), filteredPeer(accountPeer, accountPeer) { + existingPeerIds.insert(accountPeer.id) + entries.append(.localPeer(accountPeer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) + index += 1 + } + } + + var numberOfLocalPeers = 0 + for renderedPeer in foundLocalPeers.peers { + if case .expand = localExpandType, numberOfLocalPeers >= 3 { + break } - var numberOfLocalPeers = 0 - for renderedPeer in foundLocalPeers.peers { - if case .expand = localExpandType, numberOfLocalPeers >= 3 { - break - } - - if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != context.account.peerId, filteredPeer(peer, accountPeer) { - if !existingPeerIds.contains(peer.id) { - existingPeerIds.insert(peer.id) - var associatedPeer: Peer? - if let associatedPeerId = peer.associatedPeerId { - associatedPeer = renderedPeer.peers[associatedPeerId] - } - entries.append(.localPeer(peer, associatedPeer, foundLocalPeers.unread[peer.id], index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) - index += 1 - numberOfLocalPeers += 1 + if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != context.account.peerId, filteredPeer(peer, accountPeer) { + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + var associatedPeer: Peer? + if let associatedPeerId = peer.associatedPeerId { + associatedPeer = renderedPeer.peers[associatedPeerId] } + entries.append(.localPeer(peer, associatedPeer, foundLocalPeers.unread[peer.id], index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) + index += 1 + numberOfLocalPeers += 1 } } + } + + for peer in foundRemotePeers.0 { + if case .expand = localExpandType, numberOfLocalPeers >= 3 { + break + } - for peer in foundRemotePeers.0 { - if case .expand = localExpandType, numberOfLocalPeers >= 3 { + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { + existingPeerIds.insert(peer.peer.id) + entries.append(.localPeer(peer.peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) + index += 1 + numberOfLocalPeers += 1 + } + } + + var numberOfGlobalPeers = 0 + index = 0 + if let _ = options?.messageTags { + } else { + for peer in foundRemotePeers.1 { + if case .expand = globalExpandType, numberOfGlobalPeers >= 3 { break } if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { existingPeerIds.insert(peer.peer.id) - entries.append(.localPeer(peer.peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) + entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType)) index += 1 - numberOfLocalPeers += 1 - } - } - - var numberOfGlobalPeers = 0 - index = 0 - if let _ = options?.messageTags { - } else { - for peer in foundRemotePeers.1 { - if case .expand = globalExpandType, numberOfGlobalPeers >= 3 { - break - } - - if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { - existingPeerIds.insert(peer.peer.id) - entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType)) - index += 1 - numberOfGlobalPeers += 1 - } + numberOfGlobalPeers += 1 } } } @@ -1478,9 +1484,12 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo return [:] }) + let previousSearchState = Atomic(value: nil) self.searchDisposable.set((foundItems |> deliverOnMainQueue).start(next: { [weak self] entriesAndFlags in if let strongSelf = self { + let previousState = previousSearchState.swap(strongSelf.searchStateValue) + let isSearching = entriesAndFlags?.1 ?? false strongSelf._isSearching.set(isSearching) @@ -1501,12 +1510,30 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo strongSelf.mediaNode.updateHistory(entries: entries, totalCount: totalCount, updateType: .Initial) } + var entriesAndFlags = entriesAndFlags + + var peers: [Peer] = [] + if let entries = entriesAndFlags?.0 { + var filteredEntries: [ChatListSearchEntry] = [] + for entry in entries { + if case let .localPeer(peer, _, _, _, _, _, _, _, _) = entry { + peers.append(peer) + } else { + filteredEntries.append(entry) + } + } + + if strongSelf.searchOptionsValue?.messageTags != nil || strongSelf.searchOptionsValue?.maxDate != nil || strongSelf.searchOptionsValue?.peer != nil { + entriesAndFlags?.0 = filteredEntries + } + } + let previousEntries = previousSearchItems.swap(entriesAndFlags?.0) let newEntries = entriesAndFlags?.0 ?? [] + let animated = (previousState?.selectedMessageIds == nil) != (strongSelf.searchStateValue.selectedMessageIds == nil) let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), searchQuery: strongSelf.searchQueryValue ?? "", context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: filter, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, - toggleExpandLocalResults: { + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, searchQuery: strongSelf.searchQueryValue ?? "", context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: filter, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: { guard let strongSelf = self else { return } @@ -1528,7 +1555,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo guard let strongSelf = self else { return } - strongSelf.updateSearchOptions(strongSelf.currentSearchOptions.withUpdatedPeerId(peer.id, peerName: peer.compactDisplayTitle), clearQuery: true) + strongSelf.updateSearchOptions(strongSelf.currentSearchOptions.withUpdatedPeer((peer.id, peer.compactDisplayTitle)), clearQuery: true) strongSelf.dismissInput?() }, searchResults: newEntries.compactMap { entry -> Message? in if case let .message(message, _, _, _, _, _, _) = entry { @@ -1543,6 +1570,14 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo strongSelf.messageContextAction(message, node: node, rect: rect, gesture: gesture) }) strongSelf.enqueueTransition(transition, firstTime: firstTime) + + let previousPossiblePeers = strongSelf.possiblePeers + strongSelf.possiblePeers = Array(peers.prefix(10)) + + strongSelf.updatedSearchOptions?(strongSelf.searchOptionsValue, strongSelf.hasSuggestions) + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } } })) @@ -1588,8 +1623,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } var messageTags: MessageTags? = strongSelf.currentSearchOptions.messageTags var maxDate: Int32? = strongSelf.currentSearchOptions.maxDate - var peerId: PeerId? = strongSelf.currentSearchOptions.peerId - var peerName: String? = strongSelf.currentSearchOptions.peerName + var peer = strongSelf.currentSearchOptions.peer var clearQuery: Bool = false switch filter { case .media: @@ -1606,11 +1640,10 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo maxDate = date clearQuery = true case let .peer(id, name): - peerId = id - peerName = name + peer = (id, name) clearQuery = true } - strongSelf.updateSearchOptions(strongSelf.currentSearchOptions.withUpdatedMessageTags(messageTags).withUpdatedMaxDate(maxDate).withUpdatedPeerId(peerId, peerName: peerName), clearQuery: clearQuery) + strongSelf.updateSearchOptions(strongSelf.currentSearchOptions.withUpdatedMessageTags(messageTags).withUpdatedMaxDate(maxDate).withUpdatedPeer(peer), clearQuery: clearQuery) } self.mediaStatusDisposable = (combineLatest(context.sharedContext.mediaManager.globalMediaPlayerState, self.searchOptions.get()) @@ -1680,7 +1713,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } private var currentSearchOptions: ChatListSearchOptions { - return self.searchOptionsValue ?? ChatListSearchOptions(peerId: nil, peerName: nil, minDate: nil, maxDate: nil, messageTags: nil) + return self.searchOptionsValue ?? ChatListSearchOptions(peer: nil, minDate: nil, maxDate: nil, messageTags: nil) } public override func searchTokensUpdated(tokens: [SearchBarToken]) { @@ -1695,8 +1728,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if !tokensIdSet.contains(ChatListTokenId.date.rawValue) && updatedOptions?.maxDate != nil { updatedOptions = updatedOptions?.withUpdatedMaxDate(nil) } - if !tokensIdSet.contains(ChatListTokenId.peer.rawValue) && updatedOptions?.peerId != nil { - updatedOptions = updatedOptions?.withUpdatedPeerId(nil, peerName: nil) + if !tokensIdSet.contains(ChatListTokenId.peer.rawValue) && updatedOptions?.peer != nil { + updatedOptions = updatedOptions?.withUpdatedPeer(nil) } self.updateSearchOptions(updatedOptions) } @@ -1731,7 +1764,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } - if let _ = options?.peerId, let peerName = options?.peerName { + if let (_, peerName) = options?.peer { tokens.append(SearchBarToken(id: ChatListTokenId.peer.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/User"), title: peerName)) } @@ -1742,7 +1775,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let title = formatter.string(from: Date(timeIntervalSince1970: Double(maxDate))) tokens.append(SearchBarToken(id: ChatListTokenId.date.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Calendar"), title: title)) - self.possibleDate = nil + self.possibleDates = [] } if clearQuery { @@ -1751,7 +1784,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.setQuery?(nil, tokens, self.searchQueryValue ?? "") } - self.updatedSearchOptions?(options, self.possibleDate != nil) + self.updatedSearchOptions?(options, self.hasSuggestions) } private func updateTheme(theme: PresentationTheme) { @@ -1791,39 +1824,32 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.selectionPanelNode?.selectedMessages = self.searchStateValue.selectedMessageIds ?? [] } - var possibleDate: Date? + var possibleDates: [(Date, String?)] = [] + var possiblePeers: [Peer] = [] override public func searchTextUpdated(text: String) { let searchQuery: String? = !text.isEmpty ? text : nil self.interaction?.searchTextHighightState = searchQuery self.searchQuery.set(.single(searchQuery)) self.searchQueryValue = searchQuery - let previousPossibleDate = self.possibleDate - do { - let dd = try NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue) - if let match = dd.firstMatch(in: text, options: [], range: NSMakeRange(0, text.utf16.count)) { - self.possibleDate = match.date - } else { - self.possibleDate = nil - } - } - catch { - self.possibleDate = nil - } - - if previousPossibleDate != self.possibleDate { - self.updatedSearchOptions?(self.searchOptionsValue, self.possibleDate != nil) + let previousPossibleDate = self.possibleDates + self.possibleDates = suggestDates(for: text, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) + + if previousPossibleDate.isEmpty != self.possibleDates.isEmpty { + self.updatedSearchOptions?(self.searchOptionsValue, self.hasSuggestions) if let (layout, navigationBarHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } - - if text.isEmpty { - self.updateSearchState { state in - var state = state - state.expandLocalSearch = false - return state - } + } + + private var hasSuggestions: Bool { + if !self.possibleDates.isEmpty && self.searchOptionsValue?.maxDate == nil { + return true + } else if !self.possiblePeers.isEmpty && self.searchOptionsValue?.peer == nil { + return true + } else { + return false } } @@ -1864,67 +1890,87 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } private func dequeueTransition() { - if let (transition, _) = self.enqueuedTransitions.first { + if let (transition, firstTime) = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() options.insert(.PreferSynchronousDrawing) options.insert(.PreferSynchronousResourceLoading) - let displayingResults = transition.displayingResults + if transition.animated { + options.insert(.AnimateInsertion) + } + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self { let searchOptions = strongSelf.searchOptionsValue strongSelf.listNode.isHidden = searchOptions?.messageTags == .photoOrVideo && (strongSelf.searchQueryValue ?? "").isEmpty strongSelf.mediaNode.isHidden = !strongSelf.listNode.isHidden + + let displayingResults = transition.displayingResults if !displayingResults { strongSelf.listNode.isHidden = true strongSelf.mediaNode.isHidden = true } - let emptyResultsTitle: String - let emptyResultsText: String - if !transition.query.isEmpty { - emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).0 - } else { - if let searchOptions = searchOptions, searchOptions.messageTags != nil && searchOptions.minDate == nil && searchOptions.maxDate == nil && searchOptions.peerId == nil { - emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResultsFilter - if searchOptions.messageTags == .photoOrVideo { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMedia - } else if searchOptions.messageTags == .webPage { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerLinks - } else if searchOptions.messageTags == .file { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerFiles - } else if searchOptions.messageTags == .music { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMusic - } else if searchOptions.messageTags == .voiceOrInstantVideo { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerVoice + let emptyResults = displayingResults && transition.isEmpty + if emptyResults { + let emptyResultsTitle: String + let emptyResultsText: String + if !transition.query.isEmpty { + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).0 + } else { + if let searchOptions = searchOptions, searchOptions.messageTags != nil && searchOptions.minDate == nil && searchOptions.maxDate == nil && searchOptions.peer == nil { + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResultsFilter + if searchOptions.messageTags == .photoOrVideo { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMedia + } else if searchOptions.messageTags == .webPage { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerLinks + } else if searchOptions.messageTags == .file { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerFiles + } else if searchOptions.messageTags == .music { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMusic + } else if searchOptions.messageTags == .voiceOrInstantVideo { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerVoice + } else { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription + } } else { + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription } - } else { - emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription } + + strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: emptyResultsTitle, font: Font.semibold(17.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: emptyResultsText, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) } - strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: emptyResultsTitle, font: Font.semibold(17.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) - strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: emptyResultsText, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } - let emptyResults = displayingResults && transition.isEmpty strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults strongSelf.emptyResultsTitleNode.isHidden = !emptyResults strongSelf.emptyResultsTextNode.isHidden = !emptyResults strongSelf.emptyResultsAnimationNode.visibility = emptyResults + if let searchOptions = searchOptions, searchOptions.messageTags != nil && searchOptions.messageTags != .photoOrVideo, transition.query.isEmpty { + if searchOptions.messageTags == .webPage { + strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Links") + } else if searchOptions.messageTags == .file { + strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Files") + } else if searchOptions.messageTags == .music || searchOptions.messageTags == .voiceOrInstantVideo { + strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Music") + } + + strongSelf.loadingNode.isHidden = !transition.isLoading + } else { + strongSelf.loadingNode.isHidden = true + } strongSelf.recentListNode.isHidden = displayingResults || strongSelf.peersFilter.contains(.excludeRecent) strongSelf.dimNode.isHidden = displayingResults strongSelf.backgroundColor = !displayingResults && strongSelf.peersFilter.contains(.excludeRecent) ? nil : strongSelf.presentationData.theme.chatList.backgroundColor - - if let (layout, navigationBarHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } } }) } @@ -2144,13 +2190,28 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo transition.updateFrame(node: self.filterContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight + 6.0), size: CGSize(width: layout.size.width, height: 37.0))) + self.loadingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: 422.0)) + let filters: [ChatListSearchFilter] - if let possibleDate = self.possibleDate { + var customFilters: [ChatListSearchFilter] = [] + if !self.possibleDates.isEmpty && self.searchOptionsValue?.maxDate == nil { let formatter = DateFormatter() formatter.timeStyle = .none formatter.dateStyle = .medium - let title = formatter.string(from: possibleDate) - filters = [.date(Int32(possibleDate.timeIntervalSince1970), title)] + + for (date, string) in self.possibleDates { + let title = string ?? formatter.string(from: date) + customFilters.append(.date(Int32(date.timeIntervalSince1970), title)) + } + } + if !self.possiblePeers.isEmpty && self.searchOptionsValue?.peer == nil { + for peer in self.possiblePeers { + customFilters.append(.peer(peer.id, peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder))) + } + } + + if !customFilters.isEmpty { + filters = customFilters } else { filters = [.media, .links, .files, .music, .voice] } @@ -2233,14 +2294,20 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) let insets = layout.insets(options: [.input]) - let emptyAnimationSpacing: CGFloat = 8.0 + var emptyAnimationHeight = self.animationSize.height + var emptyAnimationSpacing: CGFloat = 8.0 + if case .landscape = layout.orientation, case .compact = layout.metrics.widthClass { + emptyAnimationHeight = 0.0 + emptyAnimationSpacing = 0.0 + } let emptyTextSpacing: CGFloat = 8.0 - let emptyTotalHeight = self.animationSize.height + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing let emptyAnimationY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0) - transition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - self.animationSize.width) / 2.0, y: emptyAnimationY), size: self.animationSize)) - transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyAnimationY + self.animationSize.height + emptyAnimationSpacing), size: emptyTitleSize)) - transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyAnimationY + self.animationSize.height + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) + let textTransition = ContainedViewLayoutTransition.immediate + textTransition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - self.animationSize.width) / 2.0, y: emptyAnimationY), size: self.animationSize)) + textTransition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize)) + textTransition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) self.emptyResultsAnimationNode.updateLayout(size: self.animationSize) if !hadValidLayout { diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index cf68bc4ba8..0023b47b7b 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -28,8 +28,8 @@ enum ChatListSearchFilter: Equatable { return 3 case .voice: return 4 - case .peer: - return 5 + case let .peer(peerId, _): + return peerId.id case let .date(date, _): return date } @@ -279,17 +279,15 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode { let verticalOffset: CGFloat = -3.0 for i in 0 ..< tabSizes.count { let (_, paneNodeSize, paneNode, wasAdded) = tabSizes[i] - var itemNodeTransition = transition - if wasAdded { - itemNodeTransition = .immediate - } - + let itemNodeTransition = transition + let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0) + verticalOffset), size: paneNodeSize) - itemNodeTransition.updateSublayerTransformScale(node: paneNode, scale: 1.0) - itemNodeTransition.updateAlpha(node: paneNode, alpha: 1.0) + if wasAdded { paneNode.frame = paneFrame paneNode.alpha = 0.0 + paneNode.subnodeTransform = CATransform3DMakeScale(0.1, 0.1, 1.0) + itemNodeTransition.updateSublayerTransformScale(node: paneNode, scale: 1.0) itemNodeTransition.updateAlpha(node: paneNode, alpha: 1.0) } else { itemNodeTransition.updateFrameAdditive(node: paneNode, frame: paneFrame) diff --git a/submodules/ChatListUI/Sources/DateSuggestion.swift b/submodules/ChatListUI/Sources/DateSuggestion.swift new file mode 100644 index 0000000000..79b709a0ef --- /dev/null +++ b/submodules/ChatListUI/Sources/DateSuggestion.swift @@ -0,0 +1,144 @@ +import Foundation +import TelegramPresentationData + +private let telegramReleaseDate = Date(timeIntervalSince1970: 1376438400.0) + +func suggestDates(for string: String, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> [(Date, String?)] { + let string = string.folding(options: .diacriticInsensitive, locale: .current).trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + if string.count < 3 { + return [] + } + + let months: [Int: (String, String)] = [ + 1: (strings.Month_GenJanuary, strings.Month_ShortJanuary), + 2: (strings.Month_GenFebruary, strings.Month_ShortFebruary), + 3: (strings.Month_GenMarch, strings.Month_ShortMarch), + 4: (strings.Month_GenApril, strings.Month_ShortApril), + 5: (strings.Month_GenMay, strings.Month_ShortMay), + 6: (strings.Month_GenJune, strings.Month_ShortJune), + 7: (strings.Month_GenJuly, strings.Month_ShortJuly), + 8: (strings.Month_GenAugust, strings.Month_ShortAugust), + 9: (strings.Month_GenSeptember, strings.Month_ShortSeptember), + 10: (strings.Month_GenOctober, strings.Month_ShortOctober), + 11: (strings.Month_GenNovember, strings.Month_ShortNovember), + 12: (strings.Month_GenDecember, strings.Month_ShortDecember) + ] + + let weekDays: [Int: (String, String)] = [ + 1: (strings.Weekday_Monday, strings.Weekday_ShortMonday), + 2: (strings.Weekday_Tuesday, strings.Weekday_ShortTuesday), + 3: (strings.Weekday_Wednesday, strings.Weekday_ShortWednesday), + 4: (strings.Weekday_Thursday, strings.Weekday_ShortThursday), + 5: (strings.Weekday_Friday, strings.Weekday_ShortFriday), + 6: (strings.Weekday_Saturday, strings.Weekday_ShortSaturday), + 7: (strings.Weekday_Sunday, strings.Weekday_ShortSunday.lowercased()), + ] + + let today = strings.Weekday_Today + let yesterday = strings.Weekday_Yesterday + let dateSeparator = dateTimeFormat.dateSeparator + + var result: [(Date, String?)] = [] + + let calendar = Calendar.current + func getUpperDate(for date: Date) -> Date { + let components = calendar.dateComponents(in: .current, from: date) + let upperComponents = DateComponents(year: components.year, month: components.month, day: components.day, hour: 23, minute: 59, second: 59) + return calendar.date(from: upperComponents)! + } + + let now = Date() + let nowComponents = calendar.dateComponents(in: .current, from: now) + guard let year = nowComponents.year else { + return [] + } + + let midnight = calendar.startOfDay(for: now) + if today.lowercased().hasPrefix(string) { + let todayDate = getUpperDate(for: midnight) + result.append((todayDate, today)) + } + if yesterday.lowercased().hasPrefix(string) { + let yesterdayMidnight = calendar.date(byAdding: .day, value: -1, to: midnight)! + let yesterdayDate = getUpperDate(for: yesterdayMidnight) + result.append((yesterdayDate, yesterday)) + } + + func getUpperMonthDate(month: Int, year: Int) -> Date { + let monthComponents = DateComponents(year: year, month: month) + let date = calendar.date(from: monthComponents)! + let range = calendar.range(of: .day, in: .month, for: date)! + let numDays = range.count + let upperComponents = DateComponents(year: year, month: month, day: numDays, hour: 23, minute: 59, second: 59) + return calendar.date(from: upperComponents)! + } + + let decimalRange = string.rangeOfCharacter(from: .decimalDigits) + if decimalRange != nil { + if string.count == 4, let value = Int(string), value <= year { + let date = getUpperMonthDate(month: 12, year: value) + if date > telegramReleaseDate { + result.append((date, "\(value)")) + } + } else if !dateSeparator.isEmpty && string.contains(dateSeparator) { + let stringComponents = string.components(separatedBy: dateSeparator) + if stringComponents.count > 1 { + let locale = Locale(identifier: strings.baseLanguageCode) + do { + let dd = try NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue) + if let match = dd.firstMatch(in: string, options: [], range: NSMakeRange(0, string.utf16.count)), let date = match.date, date > telegramReleaseDate { + var resultDate = date + if resultDate > now { + if let date = calendar.date(byAdding: .year, value: -1, to: resultDate) { + resultDate = date + } + } + + for i in 0..<5 { + if let date = calendar.date(byAdding: .year, value: -i, to: resultDate) { + result.append((date, nil)) + } + } + } + } catch { + + } + } + } + } else { + for (day, value) in weekDays { + let dayName = value.0.lowercased() + let shortDayName = value.1.lowercased() + if string == shortDayName || (string.count >= shortDayName.count && dayName.hasPrefix(string)) { + var nextDateComponent = calendar.dateComponents([.hour, .minute, .second], from: now) + nextDateComponent.weekday = day + calendar.firstWeekday + if let date = calendar.nextDate(after: now, matching: nextDateComponent, matchingPolicy: .nextTime, direction: .backward) { + let upperDate = getUpperDate(for: date) + for i in 0..<5 { + if let date = calendar.date(byAdding: .hour, value: -24 * 7 * i, to: upperDate) { + if calendar.isDate(date, equalTo: now, toGranularity: .weekOfYear) { + result.append((date, value.0)) + } else { + result.append((date, nil)) + } + } + } + } + } + } + for (month, value) in months { + let monthName = value.0.lowercased() + let shortMonthName = value.1.lowercased() + if string == shortMonthName || (string.count >= shortMonthName.count && monthName.hasPrefix(string)) { + for i in (year - 7 ... year).reversed() { + let date = getUpperMonthDate(month: month, year: i) + if date <= now && date > telegramReleaseDate { + result.append((date, nil)) + } + } + } + } + } + + return result +} diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 199d6d2da8..58eb59e1c9 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -1218,10 +1218,11 @@ open class NavigationBar: ASDisplayNode { public func setSecondaryContentNode(_ secondaryContentNode: ASDisplayNode?, animated: Bool = false) { if self.secondaryContentNode !== secondaryContentNode { if let previous = self.secondaryContentNode { - if animated { + if animated && previous.supernode === self.clippingNode { previous.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak previous] finished in if finished { previous?.removeFromSupernode() + previous?.layer.removeAllAnimations() } }) } else { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index a1eab00214..45c43d96d7 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -99,7 +99,7 @@ public final class HashtagSearchController: TelegramBaseController { }) let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, searchQuery: "", context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: { + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, searchQuery: "", context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: { }, toggleExpandGlobalResults: { }, searchPeer: { _ in diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 1033c448cb..e625ab6971 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -438,12 +438,12 @@ public final class ListMessageFileItemNode: ListMessageNode { } if isInstantVideo || isVoice { - let authorName: String + var authorName: String if let author = message.forwardInfo?.author { if author.id == item.context.account.peerId { authorName = item.presentationData.strings.DialogList_You } else { - authorName = author.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) + authorName = author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) } } else if let signature = message.forwardInfo?.authorSignature { authorName = signature @@ -451,11 +451,16 @@ public final class ListMessageFileItemNode: ListMessageNode { if author.id == item.context.account.peerId { authorName = item.presentationData.strings.DialogList_You } else { - authorName = author.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) + authorName = author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) } } else { authorName = " " } + + if item.isGlobalSearchResult { + authorName = fullAuthorString(for: item) + } + titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor) let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) var descriptionString: String = "" @@ -471,15 +476,6 @@ public final class ListMessageFileItemNode: ListMessageNode { } } - if item.isGlobalSearchResult { - let authorString = fullAuthorString(for: item) - if descriptionString.isEmpty { - descriptionString = authorString - } else { - descriptionString = "\(descriptionString) • \(authorString)" - } - } - descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) iconImage = .roundVideo(file) } else if !isAudio { @@ -587,7 +583,7 @@ public final class ListMessageFileItemNode: ListMessageNode { let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - rightInset - dateNodeLayout.size.width - 4.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - rightInset - dateNodeLayout.size.width - 4.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index 098d40e567..7d4706b13b 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -445,7 +445,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0 - dateNodeLayout.size.width, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - 8.0 - params.rightInset - 16.0 - dateNodeLayout.size.width, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0 - 8.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))) diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index 8eb824df4e..1fe6111dd9 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -107,6 +107,22 @@ private final class TokenNode: ASDisplayNode { self.tapped?() } + func animateIn() { + let targetFrame = self.frame + self.layer.animateFrame(from: CGRect(origin: targetFrame.origin, size: CGSize(width: 1.0, height: targetFrame.height)), to: targetFrame, 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.titleNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + func animateOut() { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak self] _ in + self?.removeFromSupernode() + }) + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + func update(theme: SearchBarNodeTheme, token: SearchBarToken, isSelected: Bool, isCollapsed: Bool) { let wasSelected = self.isSelected self.isSelected = isSelected @@ -211,7 +227,7 @@ private class SearchBarTextField: UITextField { var theme: SearchBarNodeTheme - private func layoutTokens(transition: ContainedViewLayoutTransition = .immediate) { + fileprivate func layoutTokens(transition: ContainedViewLayoutTransition = .immediate) { for i in 0 ..< self.tokens.count { let token = self.tokens[i] @@ -227,7 +243,7 @@ private class SearchBarTextField: UITextField { self?.becomeFirstResponder() } let isSelected = i == self.selectedTokenIndex - let isCollapsed = !isSelected && i < self.tokens.count - 1 + let isCollapsed = !isSelected && (i < self.tokens.count - 1 || !(self.text?.isEmpty ?? true)) tokenNode.update(theme: self.theme, token: token, isSelected: isSelected, isCollapsed: isCollapsed) } var removeKeys: [AnyHashable] = [] @@ -238,10 +254,11 @@ private class SearchBarTextField: UITextField { } for id in removeKeys { if let itemNode = self.tokenNodes.removeValue(forKey: id) { - transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in - itemNode?.removeFromSupernode() - }) - transition.updateTransformScale(node: itemNode, scale: 0.1) + if transition.isAnimated { + itemNode.animateOut() + } else { + itemNode.removeFromSupernode() + } } } @@ -297,11 +314,7 @@ private class SearchBarTextField: UITextField { } else { tokenNode.frame = nodeFrame } - tokenNode.alpha = 0.0 - tokenNodeTransition.updateAlpha(node: tokenNode, alpha: 1.0) - - tokenNode.subnodeTransform = CATransform3DMakeScale(0.1, 0.1, 1.0) - tokenNodeTransition.updateSublayerTransformScale(node: tokenNode, scale: 1.0) + tokenNode.animateIn() } else { if nodeFrame.width < tokenNode.frame.width { horizontalOffset += tokenNode.frame.width - nodeFrame.width @@ -405,6 +418,11 @@ private class SearchBarTextField: UITextField { return self.textRect(forBounds: bounds) } + override func drawText(in rect: CGRect) { + super.drawText(in: rect) + print(rect.debugDescription) + } + override func layoutSubviews() { super.layoutSubviews() @@ -582,9 +600,8 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { get { return self.textField.tokens } set { - let oldValue = self.textField.tokens self.textField.tokens = newValue - self.updateIsEmpty(animated: newValue.isEmpty && !oldValue.isEmpty) + self.updateIsEmpty(animated: true) } } @@ -687,7 +704,6 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.clearButton.imageNode.displaysAsynchronously = false self.clearButton.imageNode.displayWithoutProcessing = true self.clearButton.displaysAsynchronously = false - self.clearButton.isHidden = true self.cancelButton = HighlightableButtonNode(pointerStyle: .default) self.cancelButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) @@ -723,6 +739,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) self.updateThemeAndStrings(theme: theme, strings: strings) + self.updateIsEmpty(animated: false) } public func updateThemeAndStrings(theme: SearchBarNodeTheme, strings: PresentationStrings) { @@ -780,7 +797,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { if let activityIndicator = self.activityIndicator { let indicatorSize = activityIndicator.measure(CGSize(width: 32.0, height: 32.0)) - transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 9.0 + UIScreenPixel, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - indicatorSize.height) / 2.0) - 1.0), size: indicatorSize)) + transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 9.0 + UIScreenPixel, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - indicatorSize.height) / 2.0)), size: indicatorSize)) } let clearSize = self.clearButton.measure(CGSize(width: 100.0, height: 100.0)) @@ -956,6 +973,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { } if let textUpdated = self.textUpdated { textUpdated(textField.text ?? "", textField.textInputMode?.primaryLanguage) + self.textField.layoutTokens(transition: .animated(duration: 0.2, curve: .easeInOut)) } } @@ -968,13 +986,24 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.textField.selectAll(nil) } + public func selectLastToken() { + if !self.textField.tokens.isEmpty { + self.textField.selectedTokenIndex = self.textField.tokens.count - 1 + self.textField.becomeFirstResponder() + } + } + private func updateIsEmpty(animated: Bool = false) { let isEmpty = (self.textField.text?.isEmpty ?? true) && self.tokens.isEmpty - let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .linear) : .immediate - transition.updateAlpha(node: self.textField.placeholderLabel, alpha: isEmpty ? 1.0 : 0.0) + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .spring) : .immediate + let placeholderTransition = !isEmpty ? .immediate : transition + placeholderTransition.updateAlpha(node: self.textField.placeholderLabel, alpha: isEmpty ? 1.0 : 0.0) - self.clearButton.isHidden = isEmpty && self.prefixString == nil + let clearIsHidden = isEmpty && self.prefixString == nil + transition.updateAlpha(node: self.clearButton, alpha: clearIsHidden ? 0.0 : 1.0) + transition.updateTransformScale(node: self.clearButton, scale: clearIsHidden ? 0.2 : 1.0) + self.clearButton.isUserInteractionEnabled = !clearIsHidden } @objc private func cancelPressed() { diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index f23364e0f1..9843a4d8fd 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -14,6 +14,7 @@ public enum SearchDisplayControllerMode { public final class SearchDisplayController { private let searchBar: SearchBarNode private let mode: SearchDisplayControllerMode + private let backgroundNode: ASDisplayNode public let contentNode: SearchDisplayControllerContentNode private var hasSeparator: Bool @@ -25,6 +26,9 @@ public final class SearchDisplayController { public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, placeholder: String? = nil, hasSeparator: Bool = false, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) { self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: hasSeparator), strings: presentationData.strings, fieldStyle: .modern, forceSeparator: hasSeparator) + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = presentationData.theme.chatList.backgroundColor + self.mode = mode self.contentNode = contentNode self.hasSeparator = hasSeparator @@ -53,9 +57,15 @@ public final class SearchDisplayController { self?.searchBar.deactivate(clear: false) } self.contentNode.setQuery = { [weak self] prefix, tokens, query in - self?.searchBar.prefixString = prefix - self?.searchBar.tokens = tokens - self?.searchBar.text = query + if let strongSelf = self { + strongSelf.searchBar.prefixString = prefix + let previousTokens = strongSelf.searchBar.tokens + strongSelf.searchBar.tokens = tokens + if previousTokens.count < tokens.count { + strongSelf.searchBar.selectLastToken() + } + strongSelf.searchBar.text = query + } } if let placeholder = placeholder { self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) @@ -79,6 +89,8 @@ public final class SearchDisplayController { public func updatePresentationData(_ presentationData: PresentationData) { self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: self.hasSeparator), strings: presentationData.strings) self.contentNode.updatePresentationData(presentationData) + + self.backgroundNode.backgroundColor = presentationData.theme.chatList.backgroundColor } public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -107,6 +119,7 @@ public final class SearchDisplayController { self.containerLayout = (layout, navigationBarFrame.maxY) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size)) self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition) } @@ -116,6 +129,7 @@ public final class SearchDisplayController { return } + insertSubnode(self.backgroundNode, false) insertSubnode(self.contentNode, false) self.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size) @@ -132,8 +146,10 @@ public final class SearchDisplayController { let contentNodePosition = self.contentNode.layer.position - self.contentNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) +// self.contentNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + self.contentNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) self.searchBar.placeholderString = placeholder.placeholderString } @@ -185,6 +201,7 @@ public final class SearchDisplayController { }) } + let backgroundNode = self.backgroundNode let contentNode = self.contentNode if animated { if let placeholder = placeholder, let (layout, navigationBarHeight) = self.containerLayout { @@ -201,7 +218,11 @@ public final class SearchDisplayController { contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in contentNode?.removeFromSupernode() }) + backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak backgroundNode] _ in + backgroundNode?.removeFromSupernode() + }) } else { + backgroundNode.removeFromSupernode() contentNode.removeFromSupernode() } } diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Contents.json new file mode 100644 index 0000000000..e4bf34c537 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Files.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Files.png b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Files.png new file mode 100644 index 0000000000..9785f3ba7c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Files.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Contents.json new file mode 100644 index 0000000000..1169595a34 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Links.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Links.png b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Links.png new file mode 100644 index 0000000000..cd6782f0c9 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Links.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Contents.json new file mode 100644 index 0000000000..573448e1ce --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Music.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Music.png b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Music.png new file mode 100644 index 0000000000..7e65a1657c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Music.png differ