From 02ca03bfb8b4128f070840f40aa894d9460df57a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 12 Sep 2020 19:43:50 +0300 Subject: [PATCH] Chat search filters improvements --- .../Sources/ChatListController.swift | 35 +- .../Sources/ChatListSearchContainerNode.swift | 335 +++++++++++------- .../ChatListSearchFiltersContainerNode.swift | 16 +- .../ChatListUI/Sources/DateSuggestion.swift | 144 ++++++++ submodules/Display/Source/NavigationBar.swift | 3 +- .../Sources/HashtagSearchController.swift | 2 +- .../Sources/ListMessageFileItemNode.swift | 22 +- .../Sources/ListMessageSnippetItemNode.swift | 2 +- .../SearchBarNode/Sources/SearchBarNode.swift | 65 +++- .../Sources/SearchDisplayController.swift | 29 +- .../Search/M_Files.imageset/Contents.json | 12 + .../Search/M_Files.imageset/Files.png | Bin 0 -> 18218 bytes .../Search/M_Links.imageset/Contents.json | 12 + .../Search/M_Links.imageset/Links.png | Bin 0 -> 17164 bytes .../Search/M_Music.imageset/Contents.json | 12 + .../Search/M_Music.imageset/Music.png | Bin 0 -> 23163 bytes 16 files changed, 493 insertions(+), 196 deletions(-) create mode 100644 submodules/ChatListUI/Sources/DateSuggestion.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Files.png create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Links.png create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Music.png 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 0000000000000000000000000000000000000000..9785f3ba7c148bc3d5bfdffab29ae7906c2b33c9 GIT binary patch literal 18218 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 WsKVVMK#sxx1ZT^R&3VQLkN!Vv3v@gH literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cd6782f0c9f4acadb1cb25d1a901c3faef764f40 GIT binary patch literal 17164 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7e65a1657c1bf6edaa69cbe9a024ec92664c6677 GIT binary patch literal 23163 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 hRB-ZbGq{!GAHpC1&~@z