From cefc01f28c603b7eb3aad53a4d60bdbd26f3efb4 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 25 Sep 2020 19:11:58 +0300 Subject: [PATCH] Search filters improvements --- .../ChatListSearchRecentPeersNode.swift | 2 + .../Sources/ChatListController.swift | 5 +- .../Sources/ChatListControllerNode.swift | 11 +- .../Sources/ChatListRecentPeersListItem.swift | 88 ++-- .../Sources/ChatListSearchContainerNode.swift | 69 ++- .../ChatListSearchFiltersContainerNode.swift | 2 +- .../Sources/ChatListSearchListPaneNode.swift | 410 +++++++++++++++--- .../Sources/ChatListSearchMediaNode.swift | 35 +- .../ChatListSearchPaneContainerNode.swift | 47 +- submodules/Display/Source/NavigationBar.swift | 8 +- .../Sources/HashtagSearchController.swift | 5 +- .../Sources/HorizontalPeerItem.swift | 10 +- .../Sources/ListMessageFileItemNode.swift | 10 +- .../Sources/ListMessageItem.swift | 11 + .../Sources/ListMessageSnippetItemNode.swift | 12 +- .../Sources/SearchDisplayController.swift | 47 +- .../Sources/SelectablePeerNode.swift | 2 +- .../Themes/ThemeGridSearchContentNode.swift | 6 +- .../TelegramCore/Sources/SearchMessages.swift | 16 +- .../Search/M_Files.imageset/Contents.json | 12 - .../Search/M_Files.imageset/Files.png | Bin 18218 -> 0 bytes .../Search/M_Links.imageset/Contents.json | 12 - .../Search/M_Links.imageset/Links.png | Bin 17164 -> 0 bytes .../Search/M_Music.imageset/Contents.json | 12 - .../Search/M_Music.imageset/Music.png | Bin 23163 -> 0 bytes .../PeerInfoGroupsInCommonPaneNode.swift | 2 +- .../PeerInfo/Panes/PeerInfoMembersPane.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 14 +- 28 files changed, 551 insertions(+), 299 deletions(-) delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Contents.json delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Files.png delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Contents.json delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Links.png delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Contents.json delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Music.png diff --git a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift index 382b02bd16..e879ce151f 100644 --- a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift +++ b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift @@ -226,6 +226,8 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { var options = ListViewDeleteAndInsertOptions() if transition.firstTime { + options.insert(.PreferSynchronousResourceLoading) + options.insert(.PreferSynchronousDrawing) options.insert(.Synchronous) options.insert(.LowLatency) } else if transition.animated { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 6d3c3c47fa..e670713562 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1703,8 +1703,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, public func deactivateSearch(animated: Bool) { if !self.displayNavigationBar { + var completion: (() -> Void)? if let searchContentNode = self.searchContentNode { - self.chatListDisplayNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated) + completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated) } let filtersIsEmpty: Bool @@ -1722,6 +1723,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate self.setDisplayNavigationBar(true, transition: transition) + completion?() + (self.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.4, curve: .spring)) } } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 9ec789289e..1341eecf9e 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1088,7 +1088,6 @@ final class ChatListControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight - insets.left += layout.safeInsets.left insets.right += layout.safeInsets.right @@ -1195,11 +1194,19 @@ final class ChatListControllerNode: ASDisplayNode { }) } - func deactivateSearch(placeholderNode: SearchBarPlaceholderNode, animated: Bool) { + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode, animated: Bool) -> (() -> Void)? { if let searchDisplayController = self.searchDisplayController { searchDisplayController.deactivate(placeholder: placeholderNode, animated: animated) self.searchDisplayController = nil self.containerNode.accessibilityElementsHidden = false + + return { [weak self] in + if let strongSelf = self, let (layout, _, _, cleanNavigationBarHeight) = strongSelf.containerLayout { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) + } + } + } else { + return nil } } diff --git a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift index b8908d7e35..32a8478310 100644 --- a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift @@ -39,7 +39,9 @@ class ChatListRecentPeersListItem: ListViewItem { node.contentSize = nodeLayout.contentSize node.insets = nodeLayout.insets - completion(node, nodeApply) + completion(node, { + return (nil, { _ in nodeApply(synchronousLoads) }) + }) } } @@ -50,8 +52,8 @@ class ChatListRecentPeersListItem: ListViewItem { async { let (nodeLayout, apply) = layout(self, params, nextItem != nil) Queue.mainQueue().async { - completion(nodeLayout, { info in - apply().1(info) + completion(nodeLayout, { _ in + apply(false) }) } } @@ -86,56 +88,54 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem == nil) self.contentSize = nodeLayout.contentSize self.insets = nodeLayout.insets - let _ = nodeApply() + let _ = nodeApply(false) } } - func asyncLayout() -> (_ item: ChatListRecentPeersListItem, _ params: ListViewItemLayoutParams, _ last: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { + func asyncLayout() -> (_ item: ChatListRecentPeersListItem, _ params: ListViewItemLayoutParams, _ last: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) { let currentItem = self.item return { [weak self] item, params, last in let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 96.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) - return (nodeLayout, { [weak self] in - var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - } - - return (nil, { _ in - if let strongSelf = self { - strongSelf.item = item - - if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor - } - - let peersNode: ChatListSearchRecentPeersNode - if let currentPeersNode = strongSelf.peersNode { - peersNode = currentPeersNode - peersNode.updateThemeAndStrings(theme: item.theme, strings: item.strings) - } else { - peersNode = ChatListSearchRecentPeersNode(context: item.context, theme: item.theme, mode: .list, strings: item.strings, peerSelected: { peer in - self?.item?.peerSelected(peer) - }, peerContextAction: { peer, node, gesture in - self?.item?.peerContextAction(peer, node, gesture) - }, isPeerSelected: { _ in - return false - }) - strongSelf.peersNode = peersNode - strongSelf.addSubnode(peersNode) - } - - peersNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize) - peersNode.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - - let separatorHeight = UIScreenPixel - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight)) - strongSelf.separatorNode.isHidden = true + var updatedTheme: PresentationTheme? + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + + return (nodeLayout, { [weak self] synchronousLoads in + if let strongSelf = self { + strongSelf.item = item + + if let _ = updatedTheme { + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor } - }) + + let peersNode: ChatListSearchRecentPeersNode + if let currentPeersNode = strongSelf.peersNode { + peersNode = currentPeersNode + peersNode.updateThemeAndStrings(theme: item.theme, strings: item.strings) + } else { + peersNode = ChatListSearchRecentPeersNode(context: item.context, theme: item.theme, mode: .list, strings: item.strings, peerSelected: { peer in + self?.item?.peerSelected(peer) + }, peerContextAction: { peer, node, gesture in + self?.item?.peerContextAction(peer, node, gesture) + }, isPeerSelected: { _ in + return false + }) + strongSelf.peersNode = peersNode + strongSelf.addSubnode(peersNode) + } + + peersNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize) + peersNode.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + let separatorHeight = UIScreenPixel + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight)) + strongSelf.separatorNode.isHidden = true + } }) } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 1f191a7ffa..3b470bbff6 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -112,6 +112,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private let statePromise = ValuePromise() private var selectedFilterKey: ChatListSearchFilterEntryId? = .filter(ChatListSearchFilter.chats.id) + private var selectedFilterKeyPromise = Promise(.filter(ChatListSearchFilter.chats.id)) private var transitionFraction: CGFloat = 0.0 private var didSetReady: Bool = false @@ -133,7 +134,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.presentInGlobalOverlay = presentInGlobalOverlay self.filterContainerNode = ChatListSearchFiltersContainerNode() - self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, peersFilter: self.peersFilter, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) + self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, peersFilter: self.peersFilter, groupId: groupId, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) self.paneContainerNode.clipsToBounds = true super.init() @@ -216,7 +217,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo }, dismissInput: { [weak self] in self?.dismissInput() }, updateSuggestedPeers: { [weak self] peers, key in - if let strongSelf = self, strongSelf.paneContainerNode.currentPaneKey == key { + if let strongSelf = self, key == .chats { strongSelf.suggestedPeers.set(.single(peers)) } }, getSelectedMessageIds: { [weak self] () -> Set? in @@ -248,6 +249,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } strongSelf.selectedFilterKey = filterKey.flatMap { .filter($0.id) } + strongSelf.selectedFilterKeyPromise.set(.single(strongSelf.selectedFilterKey)) strongSelf.transitionFraction = transitionFraction if let (layout, _) = strongSelf.validLayout { @@ -257,7 +259,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } else { filters = [.chats, .media, .links, .files, .music, .voice] } - strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width, height: 38.0), sideInset: layout.safeInsets.left, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilterKey, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition) + strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilterKey, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition) } } } @@ -297,15 +299,15 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } - self.suggestedFiltersDisposable.set((combineLatest(self.suggestedPeers.get(), self.suggestedDates.get()) - |> mapToSignal { peers, dates -> Signal<([Peer], [(Date, String?)]), NoError> in + self.suggestedFiltersDisposable.set((combineLatest(self.suggestedPeers.get(), self.suggestedDates.get(), self.selectedFilterKeyPromise.get()) + |> mapToSignal { peers, dates, selectedFilter -> Signal<([Peer], [(Date, String?)], ChatListSearchFilterEntryId?), NoError> in if (peers.isEmpty && dates.isEmpty) || peers.isEmpty { - return .single((peers, dates)) + return .single((peers, dates, selectedFilter)) } else { return (.complete() |> delay(0.2, queue: Queue.mainQueue())) - |> then(.single((peers, dates))) + |> then(.single((peers, dates, selectedFilter))) } - } |> map { peers, dates -> [ChatListSearchFilter] in + } |> map { peers, dates, selectedFilter -> [ChatListSearchFilter] in var suggestedFilters: [ChatListSearchFilter] = [] if !dates.isEmpty { let formatter = DateFormatter() @@ -317,7 +319,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo suggestedFilters.append(.date(Int32(date.timeIntervalSince1970), title)) } } - if !peers.isEmpty { + if !peers.isEmpty && selectedFilter != .filter(ChatListSearchFilter.chats.id) { for peer in peers { let isGroup: Bool if let channel = peer as? TelegramChannel, case .group = channel.info { @@ -355,6 +357,18 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } })) + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme { + strongSelf.updateTheme(theme: presentationData.theme) + } + } + }) + self._ready.set(self.paneContainerNode.isReady.get() |> map { _ in Void() }) } @@ -429,29 +443,16 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } -// private func updateTheme(theme: PresentationTheme) { -// self.backgroundColor = self.peersFilter.contains(.excludeRecent) ? nil : theme.chatList.backgroundColor -// self.dimNode.backgroundColor = self.peersFilter.contains(.excludeRecent) ? UIColor.black.withAlphaComponent(0.5) : theme.chatList.backgroundColor -// self.recentListNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor -// self.listNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor -// -// self.listNode.forEachItemHeaderNode({ itemHeaderNode in -// if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { -// itemHeaderNode.updateTheme(theme: theme) -// } else if let itemHeaderNode = itemHeaderNode as? ListMessageDateHeaderNode { -// itemHeaderNode.updateThemeAndStrings(theme: theme, strings: self.presentationData.strings) -// } -// }) -// self.recentListNode.forEachItemHeaderNode({ itemHeaderNode in -// if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { -// itemHeaderNode.updateTheme(theme: theme) -// } -// }) -// } + private func updateTheme(theme: PresentationTheme) { + self.backgroundColor = self.peersFilter.contains(.excludeRecent) ? nil : theme.chatList.backgroundColor + + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } 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 @@ -463,11 +464,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.validLayout = (layout, navigationBarHeight) - var topInset = navigationBarHeight - var topPanelHeight: CGFloat = 0.0 - - topInset += topPanelHeight - + let topInset = navigationBarHeight transition.updateFrame(node: self.filterContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight + 6.0), size: CGSize(width: layout.size.width, height: 38.0))) let filters: [ChatListSearchFilter] @@ -477,7 +474,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo filters = [.chats, .media, .links, .files, .music, .voice] } - self.filterContainerNode.update(size: CGSize(width: layout.size.width, height: 38.0), sideInset: layout.safeInsets.left, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilterKey, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + self.filterContainerNode.update(size: CGSize(width: layout.size.width, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilterKey, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) if let selectedMessageIds = self.stateValue.selectedMessageIds { var wasAdded = false @@ -546,7 +543,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo }) } - transition.updateFrame(node: self.paneContainerNode, frame: CGRect(x: 0.0, y: topInset - 48.0, width: layout.size.width, height: layout.size.height - topInset + 48)) + transition.updateFrame(node: self.paneContainerNode, frame: CGRect(x: 0.0, y: topInset, width: layout.size.width, height: layout.size.height - topInset)) self.paneContainerNode.update(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), sideInset: layout.safeInsets.left, bottomInset: layout.inputHeight ?? 0.0, visibleHeight: layout.size.height - topInset, presentationData: self.presentationData, transition: transition) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 1a85fa03dd..9d75cb8946 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -367,7 +367,7 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode { } else { if self.bounds.intersects(self.scrollNode.convert(paneFrame, to: self)) { itemNodeTransition.updateFrameAdditive(node: paneNode, frame: paneFrame) - } else { + } else if paneNode.frame != paneFrame { paneNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4) { [weak paneNode] _ in paneNode?.frame = paneFrame } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 304b8e228a..e6458be46f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -24,6 +24,7 @@ import PhoneNumberFormat import InstantPageUI import GalleryData import AppBundle +import ShimmerEffect private enum ChatListRecentEntryStableId: Hashable { case topPeers @@ -365,7 +366,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } } - public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, tagMask: MessageTags?, 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)?) -> ListViewItem { + public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, tagMask: MessageTags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (Peer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)?) -> ListViewItem { switch self { case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): let primaryPeer: Peer @@ -503,10 +504,10 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case let .message(message, peer, readState, presentationData, totalCount, selected, displayCustomHeader): let header = ChatListSearchItemHeader(type: .messages(totalCount), theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none - if let tagMask = tagMask, tagMask != .photoOrVideo { + if let tagMask = tagMask, tagMask != .photoOrVideo && (searchQuery?.isEmpty ?? true) { return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message, selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: true) } else { - return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, filterData: nil, index: ChatListIndex(pinningIndex: nil, messageIndex: message.index), content: .peer(messages: [message], peer: peer, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: header, enableContextActions: false, hiddenOffset: false, interaction: interaction) + return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, filterData: nil, index: ChatListIndex(pinningIndex: nil, messageIndex: message.index), content: .peer(messages: [message], peer: peer, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) } case let .addContact(phoneNumber, theme, strings): return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { @@ -529,10 +530,10 @@ public struct ChatListSearchContainerTransition { public let displayingResults: 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, isLoading: Bool, query: String, animated: Bool) { + 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 @@ -554,12 +555,12 @@ 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, isLoading: Bool, animated: Bool, searchQuery: String, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, tagMask: MessageTags?, 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, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, tagMask: MessageTags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (Peer) -> Void, searchQuery: String?, 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, tagMask: tagMask, 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, tagMask: tagMask, 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, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, 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, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction), directionHint: nil) } return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) } @@ -636,10 +637,11 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var presentationData: PresentationData private let key: ChatListSearchPaneKey private let tagMask: MessageTags? + private let groupId: PeerGroupId? private let navigationController: NavigationController? private let recentListNode: ListView - private let loadingNode: ASImageNode + private let shimmerNode: ChatListSearchShimmerNode private let listNode: ListView private let mediaNode: ChatListSearchMediaNode private var enqueuedRecentTransitions: [(ChatListSearchContainerRecentTransition, Bool)] = [] @@ -696,11 +698,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var hiddenMediaDisposable: Disposable? - init(context: AccountContext, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, groupId: PeerGroupId?, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { self.context = context self.interaction = interaction self.key = key self.peersFilter = peersFilter + self.groupId = groupId self.navigationController = navigationController let tagMask: MessageTags? @@ -730,8 +733,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.recentListNode = ListView() self.recentListNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor - self.loadingNode = ASImageNode() - + self.shimmerNode = ChatListSearchShimmerNode(key: key) + self.shimmerNode.isUserInteractionEnabled = false + self.listNode = ListView() self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor @@ -775,7 +779,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.addSubnode(self.recentListNode) self.addSubnode(self.listNode) self.addSubnode(self.mediaNode) - self.addSubnode(self.loadingNode) + if key != .chats { + self.addSubnode(self.shimmerNode) + } self.addSubnode(self.mediaAccessoryPanelContainer) self.addSubnode(self.emptyResultsAnimationNode) @@ -871,11 +877,18 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if let (peerId, _, _) = options.peer { location = .peer(peerId: peerId, fromId: nil, tags: self.tagMask, topMsgId: nil, minDate: options.minDate?.0, maxDate: options.maxDate?.0) } else { - - location = .general(tags: self.tagMask, minDate: options.minDate?.0, maxDate: options.maxDate?.0) + if let groupId = groupId { + location = .group(groupId: groupId, tags: self.tagMask, minDate: options.minDate?.0, maxDate: options.maxDate?.0) + } else { + location = .general(tags: self.tagMask, minDate: options.minDate?.0, maxDate: options.maxDate?.0) + } } } else { - location = .general(tags: self.tagMask, minDate: nil, maxDate: nil) + if let groupId = groupId { + location = .group(groupId: groupId, tags: self.tagMask, minDate: nil, maxDate: nil) + } else { + location = .general(tags: self.tagMask, minDate: nil, maxDate: nil) + } } let finalQuery = query ?? "" @@ -1272,8 +1285,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let previousSelectedMessages = Atomic?>(value: nil) let _ = (searchQuery - |> deliverOnMainQueue).start(next: { [weak self] query in + |> deliverOnMainQueue).start(next: { [weak self, weak chatListInteraction] query in self?.searchQueryValue = query + chatListInteraction?.searchTextHighightState = query }) let _ = (searchOptions @@ -1329,7 +1343,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let animated = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil) let firstTime = previousEntries == nil - 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: peersFilter, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { _, _, _, _ in }, toggleExpandLocalResults: { + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture in + interaction.peerContextAction?(message, node, rect, gesture) + }, toggleExpandLocalResults: { guard let strongSelf = self else { return } @@ -1348,13 +1364,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return state } }, searchPeer: { peer in - }, searchResults: newEntries.compactMap { entry -> Message? in - if case let .message(message, _, _, _, _, _, _) = entry { - return message - } else { - return nil - } - }, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture in + }, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture in interaction.messageContextAction(message, node, rect, gesture) }) strongSelf.enqueueTransition(transition, firstTime: firstTime) @@ -1370,7 +1380,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if !strongSelf.didSetReady { strongSelf.ready.set(.single(true)) strongSelf.didSetReady = true - } else if tagMask != nil { + } else if tagMask == nil { interaction.updateSuggestedPeers(Array(peers.prefix(8)), strongSelf.key) } } @@ -1485,13 +1495,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { - let previousTheme = strongSelf.presentationData.theme strongSelf.presentationData = presentationData strongSelf.presentationDataPromise.set(.single(ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations))) - - if previousTheme !== presentationData.theme { -// strongSelf.updateTheme(theme: presentationData.theme) - } } }) @@ -1600,18 +1605,21 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } func scrollToTop() -> Bool { + if !self.mediaNode.isHidden { + return self.mediaNode.scrollToTop() + } let offset = self.listNode.visibleContentOffset() switch offset { case let .known(value) where value <= CGFloat.ulpOfOne: return false default: -// self.listNode.scrollto + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) return true } } func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - var hadValidLayout = self.currentParams != nil + let hadValidLayout = self.currentParams != nil self.currentParams = (size, sideInset, bottomInset, visibleHeight, presentationData) var topPanelHeight: CGFloat = 0.0 @@ -1814,11 +1822,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { transition.updateFrame(node: self.mediaAccessoryPanelContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: MediaNavigationAccessoryHeaderNode.minimizedHeight))) - var topInset: CGFloat = 0.0 - let navigationBarHeight: CGFloat = 0.0 + let topInset: CGFloat = topPanelHeight + let overflowInset: CGFloat = 20.0 let insets = UIEdgeInsets(top: topPanelHeight, left: sideInset, bottom: bottomInset, right: sideInset) - self.loadingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: 422.0)) + self.shimmerNode.frame = CGRect(origin: CGPoint(x: overflowInset, y: topInset), size: CGSize(width: size.width - overflowInset * 2.0, height: size.height)) + self.shimmerNode.update(context: self.context, size: CGSize(width: size.width - overflowInset * 2.0, height: size.height), presentationData: self.presentationData, key: (self.searchQueryValue?.isEmpty ?? true) ? self.key : .chats, transition: transition) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.recentListNode.frame = CGRect(origin: CGPoint(), size: size) @@ -1842,7 +1851,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { // } let emptyTextSpacing: CGFloat = 8.0 let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing - let emptyAnimationY = navigationBarHeight + floorToScreenPixels((visibleHeight - navigationBarHeight - bottomInset - emptyTotalHeight) / 2.0) + let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0) let textTransition = ContainedViewLayoutTransition.immediate textTransition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - self.animationSize.width) / 2.0, y: emptyAnimationY), size: self.animationSize)) @@ -1909,6 +1918,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var options = ListViewDeleteAndInsertOptions() if firstTime { + options.insert(.PreferSynchronousResourceLoading) options.insert(.PreferSynchronousDrawing) } else { options.insert(.AnimateInsertion) @@ -1957,9 +1967,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if emptyResults { let emptyResultsTitle: String let emptyResultsText: String - if !transition.query.isEmpty { + if let query = transition.query, !query.isEmpty { emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).0 + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(query).0 } else { if let searchOptions = searchOptions, searchOptions.minDate == nil && searchOptions.maxDate == nil && searchOptions.peer == nil { emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResultsFilter @@ -1994,16 +2004,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.emptyResultsTitleNode.isHidden = !emptyResults strongSelf.emptyResultsTextNode.isHidden = !emptyResults strongSelf.emptyResultsAnimationNode.visibility = emptyResults - - if strongSelf.tagMask == .webPage { - strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Links") - } else if strongSelf.tagMask == .file { - strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Files") - } else if strongSelf.tagMask == .music || strongSelf.tagMask == .voiceOrInstantVideo { - strongSelf.loadingNode.image = UIImage(bundleImageName: "Chat List/Search/M_Music") - } - - strongSelf.loadingNode.isHidden = !transition.isLoading + + ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateAlpha(node: strongSelf.shimmerNode, alpha: transition.isLoading ? 1.0 : 0.0) strongSelf.recentListNode.isHidden = displayingResults || strongSelf.peersFilter.contains(.excludeRecent) // strongSelf.dimNode.isHidden = displayingResults @@ -2058,3 +2060,313 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return nil } } + +private final class ShimmerEffectNode: ASDisplayNode { + private var currentBackgroundColor: UIColor? + private var currentForegroundColor: UIColor? + private let imageNodeContainer: ASDisplayNode + private let imageNode: ASImageNode + + private var absoluteLocation: (CGRect, CGSize)? + private var isCurrentlyInHierarchy = false + private var shouldBeAnimating = false + + override init() { + self.imageNodeContainer = ASDisplayNode() + self.imageNodeContainer.isLayerBacked = true + + self.imageNode = ASImageNode() + self.imageNode.isLayerBacked = true + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + self.imageNode.contentMode = .scaleToFill + + super.init() + + self.isLayerBacked = true + self.clipsToBounds = true + + self.imageNodeContainer.addSubnode(self.imageNode) + self.addSubnode(self.imageNodeContainer) + } + + override func didEnterHierarchy() { + super.didEnterHierarchy() + + self.isCurrentlyInHierarchy = true + self.updateAnimation() + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + self.isCurrentlyInHierarchy = false + self.updateAnimation() + } + + func update(backgroundColor: UIColor, foregroundColor: UIColor) { + if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { + return + } + self.currentBackgroundColor = backgroundColor + self.currentForegroundColor = foregroundColor + + self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.clip(to: CGRect(origin: CGPoint(), size: size)) + + let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor + let peakColor = foregroundColor.cgColor + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [transparentColor, peakColor, transparentColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { + return + } + let sizeUpdated = self.absoluteLocation?.1 != containerSize + let frameUpdated = self.absoluteLocation?.0 != rect + self.absoluteLocation = (rect, containerSize) + + if sizeUpdated { + if self.shouldBeAnimating { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + self.addImageAnimation() + } + } + + if frameUpdated { + self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) + } + + self.updateAnimation() + } + + private func updateAnimation() { + let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil + if shouldBeAnimating != self.shouldBeAnimating { + self.shouldBeAnimating = shouldBeAnimating + if shouldBeAnimating { + self.addImageAnimation() + } else { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + } + } + } + + private func addImageAnimation() { + guard let containerSize = self.absoluteLocation?.1 else { + return + } + let gradientHeight: CGFloat = 250.0 + self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight)) + let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.imageNode.layer.add(animation, forKey: "shimmer") + } +} + +private final class ChatListSearchShimmerNode: ASDisplayNode { + private let backgroundColorNode: ASDisplayNode + private let effectNode: ShimmerEffectNode + private let maskNode: ASImageNode + private var currentParams: (size: CGSize, presentationData: PresentationData, key: ChatListSearchPaneKey)? + + init(key: ChatListSearchPaneKey) { + self.backgroundColorNode = ASDisplayNode() + self.effectNode = ShimmerEffectNode() + self.maskNode = ASImageNode() + + super.init() + + self.isUserInteractionEnabled = false + + self.addSubnode(self.backgroundColorNode) + self.addSubnode(self.effectNode) + self.addSubnode(self.maskNode) + } + + func update(context: AccountContext, size: CGSize, presentationData: PresentationData, key: ChatListSearchPaneKey, transition: ContainedViewLayoutTransition) { + if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData || self.currentParams?.key != key { + self.currentParams = (size, presentationData, key) + + let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) + + let peer1 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 1), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let timestamp1: Int32 = 100000 + let peers = SimpleDictionary() + let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in + }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in + gesture?.cancel() + }, present: { _ in }) + + let items = (0 ..< 1).compactMap { _ -> ListViewItem? in + switch key { + case .chats: + return ChatListItem(presentationData: chatListPresentationData, context: context, peerGroupId: .root, filterData: nil, index: ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(messages: [Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer1, text: "Text", attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])], peer: RenderedPeer(peer: peer1), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false))]), isRemovedFromTotalUnreadCount: false, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) + case .media: + return nil + case .links: + var media: [Media] = [] + media.append(TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, attributes: [], instantPage: nil)))) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer1, text: "Text", attributes: [], media: media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(peer1.id), interaction: ListMessageItemInteraction.default, message: message, selection: .none, displayHeader: false, customHeader: nil, hintIsLink: true, isGlobalSearchResult: true) + case .files: + var media: [Media] = [] + media.append(TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: false, duration: 0, title: nil, performer: nil, waveform: MemoryBuffer(data: Data()))])) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer1, text: "Text", attributes: [], media: media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(peer1.id), interaction: ListMessageItemInteraction.default, message: message, selection: .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true) + case .music: + var media: [Media] = [] + media.append(TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: false, duration: 0, title: nil, performer: nil, waveform: MemoryBuffer(data: Data()))])) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer1, text: "Text", attributes: [], media: media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(peer1.id), interaction: ListMessageItemInteraction.default, message: message, selection: .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true) + case .voice: + var media: [Media] = [] + media.append(TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: true, duration: 0, title: nil, performer: nil, waveform: MemoryBuffer(data: Data()))])) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer1, text: "Text", attributes: [], media: media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(peer1.id), interaction: ListMessageItemInteraction.default, message: message, selection: .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true) + } + } + + var itemNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNodes.append(node) + apply().1(ListViewItemApply(isOnScreen: true)) + }) + } + + self.backgroundColorNode.backgroundColor = presentationData.theme.list.mediaPlaceholderColor + + self.maskNode.image = generateImage(size, rotatedContext: { size, context in + context.setFillColor(presentationData.theme.chatList.backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + if key == .media { + var currentY: CGFloat = 0.0 + var rowIndex: Int = 0 + + let itemSpacing: CGFloat = 1.0 + let itemsInRow = max(3, min(6, Int(size.width / 140.0))) + let itemSize: CGFloat = floor(size.width / CGFloat(itemsInRow)) + + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + + while currentY < size.height { + for i in 0 ..< itemsInRow { + let itemOrigin = CGPoint(x: CGFloat(i) * (itemSize + itemSpacing), y: itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing)) + context.fill(CGRect(origin: itemOrigin, size: CGSize(width: itemSize, height: itemSize))) + } + currentY += itemSize + rowIndex += 1 + } + } else { + var currentY: CGFloat = 0.0 + let fakeLabelPlaceholderHeight: CGFloat = 8.0 + + func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) { + let startPoint = origin + let diameter = fakeLabelPlaceholderHeight + context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter))) + } + + while currentY < size.height { + let sampleIndex = 0 + let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height + + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + + if let itemNode = itemNodes[sampleIndex] as? ChatListItemNode { + context.fillEllipse(in: itemNode.avatarNode.frame.offsetBy(dx: 0.0, dy: currentY)) + let titleFrame = itemNode.titleNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0) + + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: currentY + itemHeight - floor(itemNode.titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0) + + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0) + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0) + + let dateFrame = itemNode.dateNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY), width: 30.0) + + context.setBlendMode(.normal) + context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor) + context.fill(itemNode.separatorNode.frame.offsetBy(dx: 0.0, dy: currentY)) + } else if let itemNode = itemNodes[sampleIndex] as? ListMessageFileItemNode { + if let media = itemNode.currentMedia as? TelegramMediaFile { + if media.isMusic || media.isVoice { + context.fillEllipse(in: CGRect(x: 12.0, y: currentY + 12.0, width: 40.0, height: 40.0)) + } else { + let path = UIBezierPath(roundedRect: CGRect(x: 12.0, y: currentY + 12.0, width: 40.0, height: 40.0), cornerRadius: 6.0) + context.addPath(path.cgPath) + context.fillPath() + } + } + + let titleFrame = itemNode.titleNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0) + + let descriptionFrame = itemNode.descriptionNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: descriptionFrame.minX, y: floor(descriptionFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 240.0) + + let dateFrame = itemNode.dateNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY), width: 30.0) + + context.setBlendMode(.normal) + context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor) + context.fill(itemNode.separatorNode.frame.offsetBy(dx: 0.0, dy: currentY)) + } else if let itemNode = itemNodes[sampleIndex] as? ListMessageSnippetItemNode { + let path = UIBezierPath(roundedRect: CGRect(x: 12.0, y: currentY + 12.0, width: 40.0, height: 40.0), cornerRadius: 6.0) + context.addPath(path.cgPath) + context.fillPath() + + let titleFrame = itemNode.titleNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 120.0) + + let linkFrame = itemNode.linkNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: linkFrame.minX, y: floor(linkFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 240.0) + + let authorFrame = itemNode.authorNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: authorFrame.minX, y: floor(authorFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0) + + let dateFrame = itemNode.dateNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY), width: 30.0) + + context.setBlendMode(.normal) + context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor) + context.fill(itemNode.separatorNode.frame.offsetBy(dx: 0.0, dy: currentY)) + } + + currentY += itemHeight + } + } + }) + + self.effectNode.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4)) + self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size) + } + transition.updateFrame(node: self.backgroundColorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + } +} diff --git a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift index f8fa5b5eee..78c8c2381a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift @@ -13,7 +13,6 @@ import TelegramStringFormatting import UniversalMediaPlayer import ListMessageItem import ChatMessageInteractiveMediaBadge -import ShimmerEffect import GridMessageSelectionNode private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) @@ -49,7 +48,6 @@ private final class VisualMediaItemNode: ASDisplayNode { private let imageNode: TransformImageNode private var statusNode: RadialStatusNode private let mediaBadgeNode: ChatMessageInteractiveMediaBadge - private var placeholderNode: ShimmerEffectNode? private var selectionNode: GridMessageSelectionNode? private let fetchStatusDisposable = MetaDisposable() @@ -179,7 +177,6 @@ private final class VisualMediaItemNode: ASDisplayNode { } func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { - self.placeholderNode?.updateAbsoluteRect(absoluteRect, within: containerSize) } func update(size: CGSize, item: VisualMediaItem, theme: PresentationTheme, synchronousLoad: Bool) { @@ -302,20 +299,9 @@ private final class VisualMediaItemNode: ASDisplayNode { self.selectionNode?.frame = CGRect(origin: CGPoint(), size: size) self.updateHiddenMedia() - - if let placeholderNode = self.placeholderNode { - self.placeholderNode = nil - placeholderNode.removeFromSupernode() - } - } else if item.isEmpty, self.placeholderNode == nil { - let placeholderNode = ShimmerEffectNode() - placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.rect(rect: CGRect(origin: CGPoint(), size: size))], size: size) - self.addSubnode(placeholderNode) - self.placeholderNode = placeholderNode } let imageFrame = CGRect(origin: CGPoint(), size: size) - self.placeholderNode?.frame = imageFrame if let (item, media, _, mediaDimensions) = self.item { self.item = (item, media, size, mediaDimensions) @@ -435,15 +421,6 @@ private final class VisualMediaItem { let message: Message? let dimensions: CGSize let aspectRatio: CGFloat - let isEmpty: Bool - - init(index: UInt32) { - self.index = index - self.message = nil - self.dimensions = CGSize(width: 100.0, height: 100.0) - self.aspectRatio = 1.0 - self.isEmpty = true - } init(message: Message) { self.index = nil @@ -461,7 +438,6 @@ private final class VisualMediaItem { } self.aspectRatio = aspectRatio self.dimensions = dimensions - self.isEmpty = false } var stableId: UInt32 { @@ -741,23 +717,16 @@ final class ChatListSearchMediaNode: ASDisplayNode, UIScrollViewDelegate { break default: self.mediaItems.removeAll() - let loading: Bool - if let entries = entries { - loading = false + + if let entries = entries { for entry in entries { if case let .message(message, _, _, _, _, _, _) = entry { self.mediaItems.append(VisualMediaItem(message: message)) } } - } else { - loading = true - for i in 0 ..< 21 { - self.mediaItems.append(VisualMediaItem(index: UInt32(i))) - } } self.itemsLayout = nil - let wasInitialized = self.initialized self.initialized = true if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams { diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index dada51b135..da9d2922ae 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -36,7 +36,7 @@ final class ChatListSearchPaneWrapper { } func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - if let (currentSize, currentSideInset, currentBottomInset, visibleHeight, currentPresentationData) = self.appliedParams { + if let (currentSize, currentSideInset, currentBottomInset, _, currentPresentationData) = self.appliedParams { if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset && currentPresentationData === presentationData { return } @@ -76,12 +76,13 @@ private final class ChatListSearchPendingPane { interaction: ChatListSearchInteraction, navigationController: NavigationController?, peersFilter: ChatListNodePeersFilter, + groupId: PeerGroupId, searchQuery: Signal, searchOptions: Signal, key: ChatListSearchPaneKey, hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void ) { - let paneNode = ChatListSearchListPaneNode(context: context, interaction: interaction, key: key, peersFilter: key == .chats ? peersFilter : [], searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + let paneNode = ChatListSearchListPaneNode(context: context, interaction: interaction, key: key, peersFilter: key == .chats ? peersFilter : [], groupId: groupId, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode) self.disposable = (paneNode.isReady @@ -100,14 +101,12 @@ private final class ChatListSearchPendingPane { final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private let context: AccountContext private let peersFilter: ChatListNodePeersFilter + private let groupId: PeerGroupId private let searchQuery: Signal private let searchOptions: Signal private let navigationController: NavigationController? var interaction: ChatListSearchInteraction? - - private let coveringBackgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode - + let isReady = Promise() var didSetIsReady = false @@ -138,23 +137,15 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD private var currentAvailablePanes: [ChatListSearchPaneKey]? - init(context: AccountContext, peersFilter: ChatListNodePeersFilter, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, peersFilter: ChatListNodePeersFilter, groupId: PeerGroupId, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { self.context = context self.peersFilter = peersFilter + self.groupId = groupId self.searchQuery = searchQuery self.searchOptions = searchOptions self.navigationController = navigationController - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - - self.coveringBackgroundNode = ASDisplayNode() - self.coveringBackgroundNode.isLayerBacked = true - super.init() - - self.addSubnode(self.separatorNode) - self.addSubnode(self.coveringBackgroundNode) } func requestSelectPane(_ key: ChatListSearchPaneKey) { @@ -256,7 +247,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD directionIsToRight = translation.x > size.width / 2.0 } } - var updated = false + if let directionIsToRight = directionIsToRight { var updatedIndex = currentIndex if directionIsToRight { @@ -267,7 +258,6 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD let switchToKey = availablePanes[updatedIndex] if switchToKey != self.currentPaneKey && self.currentPanes[switchToKey] != nil{ self.currentPaneKey = switchToKey - updated = true } } self.transitionFraction = 0.0 @@ -338,19 +328,10 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD } self.currentParams = (size, sideInset, bottomInset, visibleHeight, presentationData) - - transition.updateAlpha(node: self.coveringBackgroundNode, alpha: 0.0) - + self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor - self.coveringBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor - self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - let tabsHeight: CGFloat = 48.0 - - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel))) - - let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight)) + let paneFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) var visiblePaneIndices: [Int] = [] var requiredPendingKeys: [ChatListSearchPaneKey] = [] @@ -386,6 +367,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD interaction: self.interaction!, navigationController: self.navigationController, peersFilter: self.peersFilter, + groupId: self.groupId, searchQuery: self.searchQuery, searchOptions: self.searchOptions, key: key, @@ -429,8 +411,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD var paneSwitchAnimationOffset: CGFloat = 0.0 var updatedCurrentIndex = currentIndex - var animatePaneTransitionOffset: CGFloat? - if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey, let pane = self.currentPanes[pendingSwitchToPaneKey] { + if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey, let _ = self.currentPanes[pendingSwitchToPaneKey] { self.pendingSwitchToPaneKey = nil previousPaneKey = self.currentPaneKey self.currentPaneKey = pendingSwitchToPaneKey @@ -463,8 +444,8 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD return } pane.isAnimatingOut = false - if let (size, sideInset, bottomInset, visibleHeight, presentationData) = strongSelf.currentParams { - if let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey), let paneIndex = availablePanes.firstIndex(of: key), abs(paneIndex - currentIndex) <= 1 { + if let _ = strongSelf.currentParams { + if let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey), let paneIndex = availablePanes.firstIndex(of: key), paneIndex == 0 || abs(paneIndex - currentIndex) <= 1 { } else { if let pane = strongSelf.currentPanes.removeValue(forKey: key) { pane.node.removeFromSupernode() diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 58eb59e1c9..86882093e0 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -836,10 +836,6 @@ open class NavigationBar: ASDisplayNode { self.validLayout = (size, defaultHeight, additionalHeight, leftInset, rightInset, appearsHidden) - if let secondaryContentNode = self.secondaryContentNode { -// transition.updateAlpha(node: secondaryContentNode, alpha: appearsHidden ? 0.0 : 1.0) - } - let apparentAdditionalHeight: CGFloat = self.secondaryContentNode != nil ? NavigationBar.defaultSecondaryContentHeight : 0.0 let leftButtonInset: CGFloat = leftInset + 16.0 @@ -1217,8 +1213,8 @@ open class NavigationBar: ASDisplayNode { public func setSecondaryContentNode(_ secondaryContentNode: ASDisplayNode?, animated: Bool = false) { if self.secondaryContentNode !== secondaryContentNode { - if let previous = self.secondaryContentNode { - if animated && previous.supernode === self.clippingNode { + if let previous = self.secondaryContentNode, previous.supernode === self.clippingNode { + if animated { previous.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak previous] finished in if finished { previous?.removeFromSupernode() diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 661e505d73..e433841dd1 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -96,11 +96,10 @@ public final class HashtagSearchController: TelegramBaseController { }) let firstTime = previousEntries == nil - 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: [], tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: { + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: { }, toggleExpandGlobalResults: { }, searchPeer: { _ in - - }, searchResults: [], searchOptions: nil, messageContextAction: nil) + }, searchQuery: "", searchOptions: nil, messageContextAction: nil) strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime) } }) diff --git a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift index bbff42ee43..9c9c87aeba 100644 --- a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift +++ b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift @@ -57,7 +57,7 @@ public final class HorizontalPeerItem: ListViewItem { Queue.mainQueue().async { completion(node, { return (nil, { _ in - apply(false) + apply(false, synchronousLoads) }) }) } @@ -73,7 +73,7 @@ public final class HorizontalPeerItem: ListViewItem { let (nodeLayout, apply) = layout(self, params) Queue.mainQueue().async { completion(nodeLayout, { _ in - apply(animation.isAnimated) + apply(animation.isAnimated, false) }) } } @@ -126,7 +126,7 @@ public final class HorizontalPeerItemNode: ListViewItemNode { self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) } - public func asyncLayout() -> (HorizontalPeerItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + public func asyncLayout() -> (HorizontalPeerItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let badgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) let onlineLayout = self.onlineNode.asyncLayout() @@ -184,11 +184,11 @@ public final class HorizontalPeerItemNode: ListViewItemNode { animateContent = true } - return (itemLayout, { animated in + return (itemLayout, { animated, synchronousLoads in if let strongSelf = self { strongSelf.item = item strongSelf.peerNode.theme = itemTheme - strongSelf.peerNode.setup(context: item.context, theme: item.theme, strings: item.strings, peer: RenderedPeer(peer: item.peer), numberOfLines: 1, synchronousLoad: false) + strongSelf.peerNode.setup(context: item.context, theme: item.theme, strings: item.strings, peer: RenderedPeer(peer: item.peer), numberOfLines: 1, synchronousLoad: synchronousLoads) strongSelf.peerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.size) strongSelf.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: false) diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index e40d65541f..f21fb4ca97 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -134,14 +134,14 @@ public final class ListMessageFileItemNode: ListMessageNode { private let offsetContainerNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode + public let separatorNode: ASDisplayNode private var selectionNode: ItemListSelectableControlNode? - private let titleNode: TextNode - private let descriptionNode: TextNode + public let titleNode: TextNode + public let descriptionNode: TextNode private let descriptionProgressNode: ImmediateTextNode - private let dateNode: TextNode + public let dateNode: TextNode private let extensionIconNode: ASImageNode private let extensionIconText: TextNode @@ -149,7 +149,7 @@ public final class ListMessageFileItemNode: ListMessageNode { private let iconStatusNode: SemanticStatusNode private var currentIconImage: FileIconImage? - private var currentMedia: Media? + public var currentMedia: Media? private let statusDisposable = MetaDisposable() private let fetchControls = Atomic(value: nil) diff --git a/submodules/ListMessageItem/Sources/ListMessageItem.swift b/submodules/ListMessageItem/Sources/ListMessageItem.swift index dc75279a93..74beffea41 100644 --- a/submodules/ListMessageItem/Sources/ListMessageItem.swift +++ b/submodules/ListMessageItem/Sources/ListMessageItem.swift @@ -28,6 +28,17 @@ public final class ListMessageItemInteraction { self.longTap = longTap self.getHiddenMedia = getHiddenMedia } + + public static var `default`: ListMessageItemInteraction = ListMessageItemInteraction(openMessage: { _, _ in + return false + }, openMessageContextMenu: { _, _, _, _, _ in + }, toggleMessagesSelection: { _, _ in + }, openUrl: { _, _, _, _ in + }, openInstantPage: { _, _ in + }, longTap: { _, _ in + }, getHiddenMedia: { () -> [MessageId : [Media]] in + return [:] + }) } public final class ListMessageItem: ListViewItem { diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index 946f8f7ecf..d473654286 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -31,17 +31,17 @@ public final class ListMessageSnippetItemNode: ListMessageNode { private var nonExtractedRect: CGRect? private let highlightedBackgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode + public let separatorNode: ASDisplayNode private var selectionNode: ItemListSelectableControlNode? - private let titleNode: TextNode - private let descriptionNode: TextNode - private let dateNode: TextNode + public let titleNode: TextNode + let descriptionNode: TextNode + public let dateNode: TextNode private let instantViewIconNode: ASImageNode - private let linkNode: TextNode + public let linkNode: TextNode private var linkHighlightingNode: LinkHighlightingNode? - private let authorNode: TextNode + public let authorNode: TextNode private let iconTextBackgroundNode: ASImageNode private let iconTextNode: TextNode diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index b6a05ca6d6..d896010a7b 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -126,8 +126,16 @@ public final class SearchDisplayController { let bounds = CGRect(origin: CGPoint(), size: layout.size) transition.updateFrame(node: self.backgroundNode, frame: bounds.insetBy(dx: -20.0, dy: -20.0)) - transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 20.0, y: 20.0), 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) + + var size = layout.size + size.width += 20.0 * 2.0 + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 20.0), size: size)) + + var safeInsets = layout.safeInsets + safeInsets.left += 20.0 + safeInsets.right += 20.0 + + self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition) } public func activate(insertSubnode: (ASDisplayNode, Bool) -> Void, placeholder: SearchBarPlaceholderNode?) { @@ -142,28 +150,35 @@ public final class SearchDisplayController { self.backgroundNode.backgroundColor = .clear } - self.contentNode.frame = CGRect(origin: CGPoint(x: 20.0, y: 20.0), size: layout.size) + var size = layout.size + size.width += 20.0 * 2.0 - self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), navigationBarHeight: navigationBarHeight, transition: .immediate) + self.contentNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 20.0), size: size) + + var safeInsets = layout.safeInsets + safeInsets.left += 20.0 + safeInsets.right += 20.0 + self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: safeInsets, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), navigationBarHeight: navigationBarHeight, transition: .immediate) var contentNavigationBarHeight = navigationBarHeight if layout.statusBarHeight == nil { contentNavigationBarHeight += 28.0 } - - if let placeholder = placeholder { - let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil) + + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + if !self.contentNode.hasDim { + self.backgroundNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - let contentNodePosition = self.backgroundNode.layer.position - - - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) - if !self.contentNode.hasDim { - self.backgroundNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - } else { - self.backgroundNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + if let placeholder = placeholder { + self.searchBar.placeholderString = placeholder.placeholderString + } + } else { + if let placeholder = placeholder { + let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil) + let contentNodePosition = self.backgroundNode.layer.position + self.backgroundNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + self.searchBar.placeholderString = placeholder.placeholderString } - self.searchBar.placeholderString = placeholder.placeholderString } let navigationBarFrame: CGRect diff --git a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift index 77c5462bd3..6f4c470742 100644 --- a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift +++ b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift @@ -114,7 +114,7 @@ public final class SelectablePeerNode: ASDisplayNode { self.textNode = ASTextNode() self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = true + self.textNode.displaysAsynchronously = false self.onlineNode = PeerOnlineMarkerNode() diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift index 86012e78f0..9b4f57668a 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift @@ -371,11 +371,7 @@ final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { override var isSearching: Signal { return self._isSearching.get() } - - public override var hasDim: Bool { - return true - } - + init(context: AccountContext, openResult: @escaping (ChatContextResult) -> Void) { self.context = context self.queryPromise = Promise(self.queryValue) diff --git a/submodules/TelegramCore/Sources/SearchMessages.swift b/submodules/TelegramCore/Sources/SearchMessages.swift index ce30fb2c06..19eb12abc5 100644 --- a/submodules/TelegramCore/Sources/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/SearchMessages.swift @@ -8,7 +8,7 @@ import SyncCore public enum SearchMessagesLocation: Equatable { case general(tags: MessageTags?, minDate: Int32?, maxDate: Int32?) - case group(PeerGroupId) + case group(groupId: PeerGroupId, tags: MessageTags?, minDate: Int32?, maxDate: Int32?) case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?, topMsgId: MessageId?, minDate: Int32?, maxDate: Int32?) case publicForwards(messageId: MessageId, datacenterId: Int?) } @@ -270,9 +270,15 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q } return combineLatest(peerMessages, additionalPeerMessages) } - case .group: - remoteSearchResult = .single((nil, nil)) - case let .general(tags, minDate, maxDate): + case let .general(tags, minDate, maxDate), let .group(_, tags, minDate, maxDate): + var flags: Int32 = 0 + let folderId: Int32? + if case let .group(groupId, _, _, _) = location { + folderId = groupId.rawValue + flags |= (1 << 0) + } else { + folderId = nil + } let filter: Api.MessagesFilter = tags.flatMap { messageFilterForTagMask($0) } ?? .inputMessagesFilterEmpty remoteSearchResult = account.postbox.transaction { transaction -> (Int32, MessageIndex?, Api.InputPeer) in var lowerBound: MessageIndex? @@ -286,7 +292,7 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q } } |> mapToSignal { (nextRate, lowerBound, inputPeer) in - return account.network.request(Api.functions.messages.searchGlobal(flags: 0, folderId: nil, q: query, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false) + return account.network.request(Api.functions.messages.searchGlobal(flags: flags, folderId: folderId, q: query, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false) |> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in return (result, nil) } 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 deleted file mode 100644 index e4bf34c537..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Files.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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 deleted file mode 100644 index 9785f3ba7c148bc3d5bfdffab29ae7906c2b33c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18218 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 WsKVVMK#sxx1ZT^R&3VQLkN!Vv3v@gH 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 deleted file mode 100644 index 1169595a34..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Links.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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 deleted file mode 100644 index cd6782f0c9f4acadb1cb25d1a901c3faef764f40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17164 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 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 deleted file mode 100644 index 573448e1ce..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat List/Search/M_Music.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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 deleted file mode 100644 index 7e65a1657c1bf6edaa69cbe9a024ec92664c6677..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23163 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 hRB-ZbGq{!GAHpC1&~@z Bool { if !self.listNode.scrollToOffsetFromTop(0.0) { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) return true } else { return false diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift index c84ffdf10a..2b41898196 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift @@ -165,7 +165,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { func scrollToTop() -> Bool { if !self.listNode.scrollToOffsetFromTop(0.0) { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) return true } else { return false diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 44ae82c9a5..600426bbdb 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -4841,11 +4841,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } private func activateSearch() { - guard let (layout, navigationBarHeight) = self.validLayout else { - return - } - - if let _ = self.searchDisplayController { + guard let (layout, navigationBarHeight) = self.validLayout, self.searchDisplayController == nil else { return } @@ -4912,16 +4908,14 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD transition.updateAlpha(node: navigationBar, alpha: 0.0) } - self.searchDisplayController?.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight + 10.0, transition: .immediate) self.searchDisplayController?.activate(insertSubnode: { [weak self] subnode, isSearchBar in if let strongSelf = self, let navigationBar = strongSelf.controller?.navigationBar { strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) } }, placeholder: nil) - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) - } + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationBarHeight, transition: .immediate) } private func deactivateSearch() { @@ -4958,7 +4952,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } if let searchDisplayController = self.searchDisplayController { - searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition) + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight + 10.0, transition: transition) if !searchDisplayController.isDeactivating { //vanillaInsets.top += (layout.statusBarHeight ?? 0.0) - navigationBarHeightDelta }