mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Chat search filters improvements
This commit is contained in:
parent
754504403c
commit
02ca03bfb8
@ -1695,12 +1695,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||||||
}
|
}
|
||||||
activate()
|
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 {
|
guard let strongSelf = self, let strongFilterContainerNode = filterContainerNode else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if currentHasSuggestions != hasSuggestions {
|
||||||
|
currentHasSuggestions = hasSuggestions
|
||||||
|
|
||||||
var node: ASDisplayNode?
|
var node: ASDisplayNode?
|
||||||
if let options = options, options.messageTags != nil && !hasDate {
|
if let options = options, options.messageTags != nil && !hasSuggestions {
|
||||||
} else {
|
} else {
|
||||||
node = strongFilterContainerNode
|
node = strongFilterContainerNode
|
||||||
}
|
}
|
||||||
@ -1718,6 +1722,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||||
strongSelf.setDisplayNavigationBar(false, transition: transition)
|
strongSelf.setDisplayNavigationBar(false, transition: transition)
|
||||||
|
@ -552,15 +552,19 @@ public struct ChatListSearchContainerTransition {
|
|||||||
public let updates: [ListViewUpdateItem]
|
public let updates: [ListViewUpdateItem]
|
||||||
public let displayingResults: Bool
|
public let displayingResults: Bool
|
||||||
public let isEmpty: Bool
|
public let isEmpty: Bool
|
||||||
|
public let isLoading: Bool
|
||||||
public let query: String
|
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.deletions = deletions
|
||||||
self.insertions = insertions
|
self.insertions = insertions
|
||||||
self.updates = updates
|
self.updates = updates
|
||||||
self.displayingResults = displayingResults
|
self.displayingResults = displayingResults
|
||||||
self.isEmpty = isEmpty
|
self.isEmpty = isEmpty
|
||||||
|
self.isLoading = isLoading
|
||||||
self.query = query
|
self.query = query
|
||||||
|
self.animated = animated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -574,14 +578,14 @@ private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [
|
|||||||
return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates)
|
return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, 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 (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||||
|
|
||||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchResults: searchResults, searchOptions: searchOptions, messageContextAction: messageContextAction), 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) }
|
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 {
|
private struct ChatListSearchContainerNodeState: Equatable {
|
||||||
@ -653,26 +657,25 @@ public enum ChatListSearchContextActionSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct ChatListSearchOptions {
|
public struct ChatListSearchOptions {
|
||||||
let peerId: PeerId?
|
let peer: (PeerId, String)?
|
||||||
let peerName: String?
|
|
||||||
let minDate: Int32?
|
let minDate: Int32?
|
||||||
let maxDate: Int32?
|
let maxDate: Int32?
|
||||||
let messageTags: MessageTags?
|
let messageTags: MessageTags?
|
||||||
|
|
||||||
func withUpdatedPeerId(_ peerId: PeerId?, peerName: String?) -> ChatListSearchOptions {
|
func withUpdatedPeer(_ peerIdAndName: (PeerId, String)?) -> ChatListSearchOptions {
|
||||||
return ChatListSearchOptions(peerId: peerId, peerName: peerName, minDate: self.minDate, maxDate: self.maxDate, messageTags: self.messageTags)
|
return ChatListSearchOptions(peer: peerIdAndName, minDate: self.minDate, maxDate: self.maxDate, messageTags: self.messageTags)
|
||||||
}
|
}
|
||||||
|
|
||||||
func withUpdatedMinDate(_ minDate: Int32?) -> ChatListSearchOptions {
|
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 {
|
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 {
|
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
|
let filterContainerNode: ChatListSearchFiltersContainerNode
|
||||||
private var selectionPanelNode: ChatListSearchMessageSelectionPanelNode?
|
private var selectionPanelNode: ChatListSearchMessageSelectionPanelNode?
|
||||||
private let recentListNode: ListView
|
private let recentListNode: ListView
|
||||||
|
private let loadingNode: ASImageNode
|
||||||
private let listNode: ListView
|
private let listNode: ListView
|
||||||
private let mediaNode: ChatListSearchMediaNode
|
private let mediaNode: ChatListSearchMediaNode
|
||||||
private let dimNode: ASDisplayNode
|
private let dimNode: ASDisplayNode
|
||||||
@ -771,6 +775,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
toggleMessageSelectionImpl?(messageId, selected)
|
toggleMessageSelectionImpl?(messageId, selected)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
self.loadingNode = ASImageNode()
|
||||||
|
|
||||||
self.listNode = ListView()
|
self.listNode = ListView()
|
||||||
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
|
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
|
||||||
|
|
||||||
@ -781,11 +787,13 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
self.mediaAccessoryPanelContainer.clipsToBounds = true
|
self.mediaAccessoryPanelContainer.clipsToBounds = true
|
||||||
|
|
||||||
self.emptyResultsTitleNode = ImmediateTextNode()
|
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.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.textAlignment = .center
|
||||||
self.emptyResultsTitleNode.isHidden = true
|
self.emptyResultsTitleNode.isHidden = true
|
||||||
|
|
||||||
self.emptyResultsTextNode = ImmediateTextNode()
|
self.emptyResultsTextNode = ImmediateTextNode()
|
||||||
|
self.emptyResultsTextNode.displaysAsynchronously = false
|
||||||
self.emptyResultsTextNode.maximumNumberOfLines = 0
|
self.emptyResultsTextNode.maximumNumberOfLines = 0
|
||||||
self.emptyResultsTextNode.textAlignment = .center
|
self.emptyResultsTextNode.textAlignment = .center
|
||||||
self.emptyResultsTextNode.isHidden = true
|
self.emptyResultsTextNode.isHidden = true
|
||||||
@ -807,6 +815,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
self.addSubnode(self.dimNode)
|
self.addSubnode(self.dimNode)
|
||||||
self.addSubnode(self.recentListNode)
|
self.addSubnode(self.recentListNode)
|
||||||
self.addSubnode(self.listNode)
|
self.addSubnode(self.listNode)
|
||||||
|
self.addSubnode(self.loadingNode)
|
||||||
self.addSubnode(self.mediaNode)
|
self.addSubnode(self.mediaNode)
|
||||||
|
|
||||||
self.addSubnode(self.mediaAccessoryPanelContainer)
|
self.addSubnode(self.mediaAccessoryPanelContainer)
|
||||||
@ -901,7 +910,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
}
|
}
|
||||||
let location: SearchMessagesLocation
|
let location: SearchMessagesLocation
|
||||||
if let options = options {
|
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)
|
location = .peer(peerId: peerId, fromId: nil, tags: options.messageTags, topMsgId: nil, minDate: options.minDate, maxDate: options.maxDate)
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
@ -930,7 +939,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
|
|
||||||
let loadMore = searchContext.get()
|
let loadMore = searchContext.get()
|
||||||
|> mapToSignal { searchContext -> Signal<(([Message], [PeerId: CombinedPeerReadState], Int32), Bool), NoError> in
|
|> 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 {
|
if let _ = searchContext.loadMoreIndex {
|
||||||
return searchMessages(account: context.account, location: location, query: finalQuery, state: searchContext.result.state, limit: 80)
|
return searchMessages(account: context.account, location: location, query: finalQuery, state: searchContext.result.state, limit: 80)
|
||||||
|> map { result, updatedState -> ChatListSearchMessagesResult in
|
|> map { result, updatedState -> ChatListSearchMessagesResult in
|
||||||
@ -1061,10 +1070,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
globalExpandType = .none
|
globalExpandType = .none
|
||||||
}
|
}
|
||||||
|
|
||||||
if options?.messageTags != nil || options?.maxDate != nil || options?.peerId != nil {
|
|
||||||
} else {
|
|
||||||
let lowercasedQuery = finalQuery.lowercased()
|
let lowercasedQuery = finalQuery.lowercased()
|
||||||
if lowercasedQuery.count > 1 && presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) {
|
if lowercasedQuery.count > 1 && (presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery)) {
|
||||||
if !existingPeerIds.contains(accountPeer.id), filteredPeer(accountPeer, accountPeer) {
|
if !existingPeerIds.contains(accountPeer.id), filteredPeer(accountPeer, accountPeer) {
|
||||||
existingPeerIds.insert(accountPeer.id)
|
existingPeerIds.insert(accountPeer.id)
|
||||||
entries.append(.localPeer(accountPeer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType))
|
entries.append(.localPeer(accountPeer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType))
|
||||||
@ -1122,7 +1129,6 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let message = resolvedMessage {
|
if let message = resolvedMessage {
|
||||||
var peer = RenderedPeer(message: message)
|
var peer = RenderedPeer(message: message)
|
||||||
@ -1478,9 +1484,12 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
return [:]
|
return [:]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let previousSearchState = Atomic<ChatListSearchContainerNodeSearchState?>(value: nil)
|
||||||
self.searchDisposable.set((foundItems
|
self.searchDisposable.set((foundItems
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] entriesAndFlags in
|
|> deliverOnMainQueue).start(next: { [weak self] entriesAndFlags in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
|
let previousState = previousSearchState.swap(strongSelf.searchStateValue)
|
||||||
|
|
||||||
let isSearching = entriesAndFlags?.1 ?? false
|
let isSearching = entriesAndFlags?.1 ?? false
|
||||||
strongSelf._isSearching.set(isSearching)
|
strongSelf._isSearching.set(isSearching)
|
||||||
|
|
||||||
@ -1501,12 +1510,30 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
strongSelf.mediaNode.updateHistory(entries: entries, totalCount: totalCount, updateType: .Initial)
|
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 previousEntries = previousSearchItems.swap(entriesAndFlags?.0)
|
||||||
let newEntries = entriesAndFlags?.0 ?? []
|
let newEntries = entriesAndFlags?.0 ?? []
|
||||||
|
|
||||||
|
let animated = (previousState?.selectedMessageIds == nil) != (strongSelf.searchStateValue.selectedMessageIds == nil)
|
||||||
let firstTime = previousEntries == 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,
|
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: {
|
||||||
toggleExpandLocalResults: {
|
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1528,7 +1555,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
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?()
|
strongSelf.dismissInput?()
|
||||||
}, searchResults: newEntries.compactMap { entry -> Message? in
|
}, searchResults: newEntries.compactMap { entry -> Message? in
|
||||||
if case let .message(message, _, _, _, _, _, _) = entry {
|
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.messageContextAction(message, node: node, rect: rect, gesture: gesture)
|
||||||
})
|
})
|
||||||
strongSelf.enqueueTransition(transition, firstTime: firstTime)
|
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 messageTags: MessageTags? = strongSelf.currentSearchOptions.messageTags
|
||||||
var maxDate: Int32? = strongSelf.currentSearchOptions.maxDate
|
var maxDate: Int32? = strongSelf.currentSearchOptions.maxDate
|
||||||
var peerId: PeerId? = strongSelf.currentSearchOptions.peerId
|
var peer = strongSelf.currentSearchOptions.peer
|
||||||
var peerName: String? = strongSelf.currentSearchOptions.peerName
|
|
||||||
var clearQuery: Bool = false
|
var clearQuery: Bool = false
|
||||||
switch filter {
|
switch filter {
|
||||||
case .media:
|
case .media:
|
||||||
@ -1606,11 +1640,10 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
maxDate = date
|
maxDate = date
|
||||||
clearQuery = true
|
clearQuery = true
|
||||||
case let .peer(id, name):
|
case let .peer(id, name):
|
||||||
peerId = id
|
peer = (id, name)
|
||||||
peerName = name
|
|
||||||
clearQuery = true
|
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())
|
self.mediaStatusDisposable = (combineLatest(context.sharedContext.mediaManager.globalMediaPlayerState, self.searchOptions.get())
|
||||||
@ -1680,7 +1713,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var currentSearchOptions: ChatListSearchOptions {
|
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]) {
|
public override func searchTokensUpdated(tokens: [SearchBarToken]) {
|
||||||
@ -1695,8 +1728,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
if !tokensIdSet.contains(ChatListTokenId.date.rawValue) && updatedOptions?.maxDate != nil {
|
if !tokensIdSet.contains(ChatListTokenId.date.rawValue) && updatedOptions?.maxDate != nil {
|
||||||
updatedOptions = updatedOptions?.withUpdatedMaxDate(nil)
|
updatedOptions = updatedOptions?.withUpdatedMaxDate(nil)
|
||||||
}
|
}
|
||||||
if !tokensIdSet.contains(ChatListTokenId.peer.rawValue) && updatedOptions?.peerId != nil {
|
if !tokensIdSet.contains(ChatListTokenId.peer.rawValue) && updatedOptions?.peer != nil {
|
||||||
updatedOptions = updatedOptions?.withUpdatedPeerId(nil, peerName: nil)
|
updatedOptions = updatedOptions?.withUpdatedPeer(nil)
|
||||||
}
|
}
|
||||||
self.updateSearchOptions(updatedOptions)
|
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))
|
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)))
|
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))
|
tokens.append(SearchBarToken(id: ChatListTokenId.date.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Calendar"), title: title))
|
||||||
|
|
||||||
self.possibleDate = nil
|
self.possibleDates = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if clearQuery {
|
if clearQuery {
|
||||||
@ -1751,7 +1784,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
self.setQuery?(nil, tokens, self.searchQueryValue ?? "")
|
self.setQuery?(nil, tokens, self.searchQueryValue ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updatedSearchOptions?(options, self.possibleDate != nil)
|
self.updatedSearchOptions?(options, self.hasSuggestions)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateTheme(theme: PresentationTheme) {
|
private func updateTheme(theme: PresentationTheme) {
|
||||||
@ -1791,39 +1824,32 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
self.selectionPanelNode?.selectedMessages = self.searchStateValue.selectedMessageIds ?? []
|
self.selectionPanelNode?.selectedMessages = self.searchStateValue.selectedMessageIds ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
var possibleDate: Date?
|
var possibleDates: [(Date, String?)] = []
|
||||||
|
var possiblePeers: [Peer] = []
|
||||||
override public func searchTextUpdated(text: String) {
|
override public func searchTextUpdated(text: String) {
|
||||||
let searchQuery: String? = !text.isEmpty ? text : nil
|
let searchQuery: String? = !text.isEmpty ? text : nil
|
||||||
self.interaction?.searchTextHighightState = searchQuery
|
self.interaction?.searchTextHighightState = searchQuery
|
||||||
self.searchQuery.set(.single(searchQuery))
|
self.searchQuery.set(.single(searchQuery))
|
||||||
self.searchQueryValue = searchQuery
|
self.searchQueryValue = searchQuery
|
||||||
|
|
||||||
let previousPossibleDate = self.possibleDate
|
let previousPossibleDate = self.possibleDates
|
||||||
do {
|
self.possibleDates = suggestDates(for: text, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)
|
||||||
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 {
|
if previousPossibleDate.isEmpty != self.possibleDates.isEmpty {
|
||||||
self.updatedSearchOptions?(self.searchOptionsValue, self.possibleDate != nil)
|
self.updatedSearchOptions?(self.searchOptionsValue, self.hasSuggestions)
|
||||||
if let (layout, navigationBarHeight) = self.validLayout {
|
if let (layout, navigationBarHeight) = self.validLayout {
|
||||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
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,31 +1890,38 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func dequeueTransition() {
|
private func dequeueTransition() {
|
||||||
if let (transition, _) = self.enqueuedTransitions.first {
|
if let (transition, firstTime) = self.enqueuedTransitions.first {
|
||||||
self.enqueuedTransitions.remove(at: 0)
|
self.enqueuedTransitions.remove(at: 0)
|
||||||
|
|
||||||
var options = ListViewDeleteAndInsertOptions()
|
var options = ListViewDeleteAndInsertOptions()
|
||||||
options.insert(.PreferSynchronousDrawing)
|
options.insert(.PreferSynchronousDrawing)
|
||||||
options.insert(.PreferSynchronousResourceLoading)
|
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
|
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 {
|
if let strongSelf = self {
|
||||||
let searchOptions = strongSelf.searchOptionsValue
|
let searchOptions = strongSelf.searchOptionsValue
|
||||||
strongSelf.listNode.isHidden = searchOptions?.messageTags == .photoOrVideo && (strongSelf.searchQueryValue ?? "").isEmpty
|
strongSelf.listNode.isHidden = searchOptions?.messageTags == .photoOrVideo && (strongSelf.searchQueryValue ?? "").isEmpty
|
||||||
strongSelf.mediaNode.isHidden = !strongSelf.listNode.isHidden
|
strongSelf.mediaNode.isHidden = !strongSelf.listNode.isHidden
|
||||||
|
|
||||||
|
let displayingResults = transition.displayingResults
|
||||||
if !displayingResults {
|
if !displayingResults {
|
||||||
strongSelf.listNode.isHidden = true
|
strongSelf.listNode.isHidden = true
|
||||||
strongSelf.mediaNode.isHidden = true
|
strongSelf.mediaNode.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let emptyResults = displayingResults && transition.isEmpty
|
||||||
|
if emptyResults {
|
||||||
let emptyResultsTitle: String
|
let emptyResultsTitle: String
|
||||||
let emptyResultsText: String
|
let emptyResultsText: String
|
||||||
if !transition.query.isEmpty {
|
if !transition.query.isEmpty {
|
||||||
emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults
|
emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults
|
||||||
emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).0
|
emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).0
|
||||||
} else {
|
} else {
|
||||||
if let searchOptions = searchOptions, searchOptions.messageTags != nil && searchOptions.minDate == nil && searchOptions.maxDate == nil && searchOptions.peerId == nil {
|
if let searchOptions = searchOptions, searchOptions.messageTags != nil && searchOptions.minDate == nil && searchOptions.maxDate == nil && searchOptions.peer == nil {
|
||||||
emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResultsFilter
|
emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResultsFilter
|
||||||
if searchOptions.messageTags == .photoOrVideo {
|
if searchOptions.messageTags == .photoOrVideo {
|
||||||
emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMedia
|
emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMedia
|
||||||
@ -1911,20 +1944,33 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||||||
|
|
||||||
strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: emptyResultsTitle, font: Font.semibold(17.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)
|
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.emptyResultsAnimationNode.isHidden = !emptyResults
|
||||||
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
|
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
|
||||||
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
|
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
|
||||||
strongSelf.emptyResultsAnimationNode.visibility = 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.recentListNode.isHidden = displayingResults || strongSelf.peersFilter.contains(.excludeRecent)
|
||||||
strongSelf.dimNode.isHidden = displayingResults
|
strongSelf.dimNode.isHidden = displayingResults
|
||||||
strongSelf.backgroundColor = !displayingResults && strongSelf.peersFilter.contains(.excludeRecent) ? nil : strongSelf.presentationData.theme.chatList.backgroundColor
|
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)))
|
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]
|
let filters: [ChatListSearchFilter]
|
||||||
if let possibleDate = self.possibleDate {
|
var customFilters: [ChatListSearchFilter] = []
|
||||||
|
if !self.possibleDates.isEmpty && self.searchOptionsValue?.maxDate == nil {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.timeStyle = .none
|
formatter.timeStyle = .none
|
||||||
formatter.dateStyle = .medium
|
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 {
|
} else {
|
||||||
filters = [.media, .links, .files, .music, .voice]
|
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 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 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 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)
|
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))
|
let textTransition = ContainedViewLayoutTransition.immediate
|
||||||
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))
|
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))
|
||||||
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))
|
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)
|
self.emptyResultsAnimationNode.updateLayout(size: self.animationSize)
|
||||||
|
|
||||||
if !hadValidLayout {
|
if !hadValidLayout {
|
||||||
|
@ -28,8 +28,8 @@ enum ChatListSearchFilter: Equatable {
|
|||||||
return 3
|
return 3
|
||||||
case .voice:
|
case .voice:
|
||||||
return 4
|
return 4
|
||||||
case .peer:
|
case let .peer(peerId, _):
|
||||||
return 5
|
return peerId.id
|
||||||
case let .date(date, _):
|
case let .date(date, _):
|
||||||
return date
|
return date
|
||||||
}
|
}
|
||||||
@ -279,17 +279,15 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode {
|
|||||||
let verticalOffset: CGFloat = -3.0
|
let verticalOffset: CGFloat = -3.0
|
||||||
for i in 0 ..< tabSizes.count {
|
for i in 0 ..< tabSizes.count {
|
||||||
let (_, paneNodeSize, paneNode, wasAdded) = tabSizes[i]
|
let (_, paneNodeSize, paneNode, wasAdded) = tabSizes[i]
|
||||||
var itemNodeTransition = transition
|
let itemNodeTransition = transition
|
||||||
if wasAdded {
|
|
||||||
itemNodeTransition = .immediate
|
|
||||||
}
|
|
||||||
|
|
||||||
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0) + verticalOffset), size: paneNodeSize)
|
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 {
|
if wasAdded {
|
||||||
paneNode.frame = paneFrame
|
paneNode.frame = paneFrame
|
||||||
paneNode.alpha = 0.0
|
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)
|
itemNodeTransition.updateAlpha(node: paneNode, alpha: 1.0)
|
||||||
} else {
|
} else {
|
||||||
itemNodeTransition.updateFrameAdditive(node: paneNode, frame: paneFrame)
|
itemNodeTransition.updateFrameAdditive(node: paneNode, frame: paneFrame)
|
||||||
|
144
submodules/ChatListUI/Sources/DateSuggestion.swift
Normal file
144
submodules/ChatListUI/Sources/DateSuggestion.swift
Normal file
@ -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
|
||||||
|
}
|
@ -1218,10 +1218,11 @@ open class NavigationBar: ASDisplayNode {
|
|||||||
public func setSecondaryContentNode(_ secondaryContentNode: ASDisplayNode?, animated: Bool = false) {
|
public func setSecondaryContentNode(_ secondaryContentNode: ASDisplayNode?, animated: Bool = false) {
|
||||||
if self.secondaryContentNode !== secondaryContentNode {
|
if self.secondaryContentNode !== secondaryContentNode {
|
||||||
if let previous = self.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
|
previous.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak previous] finished in
|
||||||
if finished {
|
if finished {
|
||||||
previous?.removeFromSupernode()
|
previous?.removeFromSupernode()
|
||||||
|
previous?.layer.removeAllAnimations()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -99,7 +99,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let firstTime = previousEntries == nil
|
let firstTime = previousEntries == nil
|
||||||
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, 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: {
|
}, toggleExpandGlobalResults: {
|
||||||
}, searchPeer: { _ in
|
}, searchPeer: { _ in
|
||||||
|
|
||||||
|
@ -438,12 +438,12 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isInstantVideo || isVoice {
|
if isInstantVideo || isVoice {
|
||||||
let authorName: String
|
var authorName: String
|
||||||
if let author = message.forwardInfo?.author {
|
if let author = message.forwardInfo?.author {
|
||||||
if author.id == item.context.account.peerId {
|
if author.id == item.context.account.peerId {
|
||||||
authorName = item.presentationData.strings.DialogList_You
|
authorName = item.presentationData.strings.DialogList_You
|
||||||
} else {
|
} 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 {
|
} else if let signature = message.forwardInfo?.authorSignature {
|
||||||
authorName = signature
|
authorName = signature
|
||||||
@ -451,11 +451,16 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
if author.id == item.context.account.peerId {
|
if author.id == item.context.account.peerId {
|
||||||
authorName = item.presentationData.strings.DialogList_You
|
authorName = item.presentationData.strings.DialogList_You
|
||||||
} else {
|
} else {
|
||||||
authorName = author.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast)
|
authorName = author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
authorName = " "
|
authorName = " "
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.isGlobalSearchResult {
|
||||||
|
authorName = fullAuthorString(for: item)
|
||||||
|
}
|
||||||
|
|
||||||
titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
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)
|
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||||
var descriptionString: String = ""
|
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)
|
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||||
iconImage = .roundVideo(file)
|
iconImage = .roundVideo(file)
|
||||||
} else if !isAudio {
|
} 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 (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()))
|
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()))
|
||||||
|
|
||||||
|
@ -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 (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)))
|
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)))
|
||||||
|
|
||||||
|
@ -107,6 +107,22 @@ private final class TokenNode: ASDisplayNode {
|
|||||||
self.tapped?()
|
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) {
|
func update(theme: SearchBarNodeTheme, token: SearchBarToken, isSelected: Bool, isCollapsed: Bool) {
|
||||||
let wasSelected = self.isSelected
|
let wasSelected = self.isSelected
|
||||||
self.isSelected = isSelected
|
self.isSelected = isSelected
|
||||||
@ -211,7 +227,7 @@ private class SearchBarTextField: UITextField {
|
|||||||
|
|
||||||
var theme: SearchBarNodeTheme
|
var theme: SearchBarNodeTheme
|
||||||
|
|
||||||
private func layoutTokens(transition: ContainedViewLayoutTransition = .immediate) {
|
fileprivate func layoutTokens(transition: ContainedViewLayoutTransition = .immediate) {
|
||||||
for i in 0 ..< self.tokens.count {
|
for i in 0 ..< self.tokens.count {
|
||||||
let token = self.tokens[i]
|
let token = self.tokens[i]
|
||||||
|
|
||||||
@ -227,7 +243,7 @@ private class SearchBarTextField: UITextField {
|
|||||||
self?.becomeFirstResponder()
|
self?.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
let isSelected = i == self.selectedTokenIndex
|
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)
|
tokenNode.update(theme: self.theme, token: token, isSelected: isSelected, isCollapsed: isCollapsed)
|
||||||
}
|
}
|
||||||
var removeKeys: [AnyHashable] = []
|
var removeKeys: [AnyHashable] = []
|
||||||
@ -238,10 +254,11 @@ private class SearchBarTextField: UITextField {
|
|||||||
}
|
}
|
||||||
for id in removeKeys {
|
for id in removeKeys {
|
||||||
if let itemNode = self.tokenNodes.removeValue(forKey: id) {
|
if let itemNode = self.tokenNodes.removeValue(forKey: id) {
|
||||||
transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in
|
if transition.isAnimated {
|
||||||
itemNode?.removeFromSupernode()
|
itemNode.animateOut()
|
||||||
})
|
} else {
|
||||||
transition.updateTransformScale(node: itemNode, scale: 0.1)
|
itemNode.removeFromSupernode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,11 +314,7 @@ private class SearchBarTextField: UITextField {
|
|||||||
} else {
|
} else {
|
||||||
tokenNode.frame = nodeFrame
|
tokenNode.frame = nodeFrame
|
||||||
}
|
}
|
||||||
tokenNode.alpha = 0.0
|
tokenNode.animateIn()
|
||||||
tokenNodeTransition.updateAlpha(node: tokenNode, alpha: 1.0)
|
|
||||||
|
|
||||||
tokenNode.subnodeTransform = CATransform3DMakeScale(0.1, 0.1, 1.0)
|
|
||||||
tokenNodeTransition.updateSublayerTransformScale(node: tokenNode, scale: 1.0)
|
|
||||||
} else {
|
} else {
|
||||||
if nodeFrame.width < tokenNode.frame.width {
|
if nodeFrame.width < tokenNode.frame.width {
|
||||||
horizontalOffset += tokenNode.frame.width - nodeFrame.width
|
horizontalOffset += tokenNode.frame.width - nodeFrame.width
|
||||||
@ -405,6 +418,11 @@ private class SearchBarTextField: UITextField {
|
|||||||
return self.textRect(forBounds: bounds)
|
return self.textRect(forBounds: bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func drawText(in rect: CGRect) {
|
||||||
|
super.drawText(in: rect)
|
||||||
|
print(rect.debugDescription)
|
||||||
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
@ -582,9 +600,8 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
|||||||
get {
|
get {
|
||||||
return self.textField.tokens
|
return self.textField.tokens
|
||||||
} set {
|
} set {
|
||||||
let oldValue = self.textField.tokens
|
|
||||||
self.textField.tokens = newValue
|
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.displaysAsynchronously = false
|
||||||
self.clearButton.imageNode.displayWithoutProcessing = true
|
self.clearButton.imageNode.displayWithoutProcessing = true
|
||||||
self.clearButton.displaysAsynchronously = false
|
self.clearButton.displaysAsynchronously = false
|
||||||
self.clearButton.isHidden = true
|
|
||||||
|
|
||||||
self.cancelButton = HighlightableButtonNode(pointerStyle: .default)
|
self.cancelButton = HighlightableButtonNode(pointerStyle: .default)
|
||||||
self.cancelButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
|
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.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||||
|
self.updateIsEmpty(animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateThemeAndStrings(theme: SearchBarNodeTheme, strings: PresentationStrings) {
|
public func updateThemeAndStrings(theme: SearchBarNodeTheme, strings: PresentationStrings) {
|
||||||
@ -780,7 +797,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
|||||||
|
|
||||||
if let activityIndicator = self.activityIndicator {
|
if let activityIndicator = self.activityIndicator {
|
||||||
let indicatorSize = activityIndicator.measure(CGSize(width: 32.0, height: 32.0))
|
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))
|
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 {
|
if let textUpdated = self.textUpdated {
|
||||||
textUpdated(textField.text ?? "", textField.textInputMode?.primaryLanguage)
|
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)
|
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) {
|
private func updateIsEmpty(animated: Bool = false) {
|
||||||
let isEmpty = (self.textField.text?.isEmpty ?? true) && self.tokens.isEmpty
|
let isEmpty = (self.textField.text?.isEmpty ?? true) && self.tokens.isEmpty
|
||||||
|
|
||||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .linear) : .immediate
|
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .spring) : .immediate
|
||||||
transition.updateAlpha(node: self.textField.placeholderLabel, alpha: isEmpty ? 1.0 : 0.0)
|
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() {
|
@objc private func cancelPressed() {
|
||||||
|
@ -14,6 +14,7 @@ public enum SearchDisplayControllerMode {
|
|||||||
public final class SearchDisplayController {
|
public final class SearchDisplayController {
|
||||||
private let searchBar: SearchBarNode
|
private let searchBar: SearchBarNode
|
||||||
private let mode: SearchDisplayControllerMode
|
private let mode: SearchDisplayControllerMode
|
||||||
|
private let backgroundNode: ASDisplayNode
|
||||||
public let contentNode: SearchDisplayControllerContentNode
|
public let contentNode: SearchDisplayControllerContentNode
|
||||||
private var hasSeparator: Bool
|
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) {
|
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.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.mode = mode
|
||||||
self.contentNode = contentNode
|
self.contentNode = contentNode
|
||||||
self.hasSeparator = hasSeparator
|
self.hasSeparator = hasSeparator
|
||||||
@ -53,9 +57,15 @@ public final class SearchDisplayController {
|
|||||||
self?.searchBar.deactivate(clear: false)
|
self?.searchBar.deactivate(clear: false)
|
||||||
}
|
}
|
||||||
self.contentNode.setQuery = { [weak self] prefix, tokens, query in
|
self.contentNode.setQuery = { [weak self] prefix, tokens, query in
|
||||||
self?.searchBar.prefixString = prefix
|
if let strongSelf = self {
|
||||||
self?.searchBar.tokens = tokens
|
strongSelf.searchBar.prefixString = prefix
|
||||||
self?.searchBar.text = query
|
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 {
|
if let placeholder = placeholder {
|
||||||
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
|
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) {
|
public func updatePresentationData(_ presentationData: PresentationData) {
|
||||||
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: self.hasSeparator), strings: presentationData.strings)
|
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: self.hasSeparator), strings: presentationData.strings)
|
||||||
self.contentNode.updatePresentationData(presentationData)
|
self.contentNode.updatePresentationData(presentationData)
|
||||||
|
|
||||||
|
self.backgroundNode.backgroundColor = presentationData.theme.chatList.backgroundColor
|
||||||
}
|
}
|
||||||
|
|
||||||
public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
@ -107,6 +119,7 @@ public final class SearchDisplayController {
|
|||||||
|
|
||||||
self.containerLayout = (layout, navigationBarFrame.maxY)
|
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))
|
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)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertSubnode(self.backgroundNode, false)
|
||||||
insertSubnode(self.contentNode, false)
|
insertSubnode(self.contentNode, false)
|
||||||
|
|
||||||
self.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
self.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||||
@ -132,8 +146,10 @@ public final class SearchDisplayController {
|
|||||||
|
|
||||||
let contentNodePosition = self.contentNode.layer.position
|
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.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
|
self.searchBar.placeholderString = placeholder.placeholderString
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,6 +201,7 @@ public final class SearchDisplayController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let backgroundNode = self.backgroundNode
|
||||||
let contentNode = self.contentNode
|
let contentNode = self.contentNode
|
||||||
if animated {
|
if animated {
|
||||||
if let placeholder = placeholder, let (layout, navigationBarHeight) = self.containerLayout {
|
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.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in
|
||||||
contentNode?.removeFromSupernode()
|
contentNode?.removeFromSupernode()
|
||||||
})
|
})
|
||||||
|
backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak backgroundNode] _ in
|
||||||
|
backgroundNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
|
backgroundNode.removeFromSupernode()
|
||||||
contentNode.removeFromSupernode()
|
contentNode.removeFromSupernode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Files.png",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Files.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Files.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
12
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Links.png",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Links.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Links.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
12
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Music.png",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Music.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Music.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
Loading…
x
Reference in New Issue
Block a user