diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 3ee7cc641d..875857ad41 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -947,6 +947,8 @@ public protocol ChatController: ViewController { var visibleContextController: ViewController? { get } + var alwaysShowSearchResultsAsList: Bool { get set } + func updatePresentationMode(_ mode: ChatControllerPresentationMode) func beginMessageSearch(_ query: String) func displayPromoAnnouncement(text: String) diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/BUILD b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/BUILD index 904cf18eb8..a43fd25787 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/TelegramUIPreferences", "//submodules/UIKitRuntimeUtils", + "//submodules/ChatPresentationInterfaceState", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 4ff623eef4..ac06ac2a05 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -12,6 +12,7 @@ import TelegramPresentationData import SwiftSignalKit import TelegramUIPreferences import UIKitRuntimeUtils +import ChatPresentationInterfaceState public final class ChatInlineSearchResultsListComponent: Component { public struct Presentation: Equatable { @@ -62,7 +63,9 @@ public final class ChatInlineSearchResultsListComponent: Component { } public enum Contents: Equatable { + case empty case tag(MemoryBuffer) + case search } public let context: AccountContext @@ -72,6 +75,7 @@ public final class ChatInlineSearchResultsListComponent: Component { public let insets: UIEdgeInsets public let messageSelected: (EngineMessage) -> Void public let loadTagMessages: (MemoryBuffer, MessageIndex?) -> Signal? + public let getSearchResult: () -> Signal? public init( context: AccountContext, @@ -80,7 +84,8 @@ public final class ChatInlineSearchResultsListComponent: Component { contents: Contents, insets: UIEdgeInsets, messageSelected: @escaping (EngineMessage) -> Void, - loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal? + loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal?, + getSearchResult: @escaping () -> Signal? ) { self.context = context self.presentation = presentation @@ -89,6 +94,7 @@ public final class ChatInlineSearchResultsListComponent: Component { self.insets = insets self.messageSelected = messageSelected self.loadTagMessages = loadTagMessages + self.getSearchResult = getSearchResult } public static func ==(lhs: ChatInlineSearchResultsListComponent, rhs: ChatInlineSearchResultsListComponent) -> Bool { @@ -111,15 +117,21 @@ public final class ChatInlineSearchResultsListComponent: Component { } private struct ContentsState: Equatable { + enum ContentId: Equatable { + case empty + case tag(MemoryBuffer) + case search + } + var id: Int - var tag: MemoryBuffer? + var contentId: ContentId var entries: [EngineMessage] var hasEarlier: Bool var hasLater: Bool - init(id: Int, tag: MemoryBuffer?, entries: [EngineMessage], hasEarlier: Bool, hasLater: Bool) { + init(id: Int, contentId: ContentId, entries: [EngineMessage], hasEarlier: Bool, hasLater: Bool) { self.id = id - self.tag = tag + self.contentId = contentId self.entries = entries self.hasEarlier = hasEarlier self.hasLater = hasLater @@ -134,6 +146,7 @@ public final class ChatInlineSearchResultsListComponent: Component { private let listNode: ListView private var tagContents: (index: MessageIndex?, disposable: Disposable?)? + private var searchContents: (index: MessageIndex?, disposable: Disposable?)? private var nextContentsId: Int = 0 private var contentsState: ContentsState? @@ -162,6 +175,7 @@ public final class ChatInlineSearchResultsListComponent: Component { deinit { self.tagContents?.disposable?.dispose() + self.searchContents?.disposable?.dispose() } public func animateIn() { @@ -228,9 +242,6 @@ public final class ChatInlineSearchResultsListComponent: Component { guard let contentsState = self.contentsState, contentsState.id == stateId else { return } - guard let (currentIndex, disposable) = self.tagContents else { - return - } guard let visibleRange = displayedRange.visibleRange else { return } @@ -245,49 +256,85 @@ public final class ChatInlineSearchResultsListComponent: Component { } } - if let loadAroundIndex, loadAroundIndex != currentIndex { - switch component.contents { - case let .tag(tag): - disposable?.dispose() - let updatedDisposable = MetaDisposable() - self.tagContents = (loadAroundIndex, updatedDisposable) - - if let historySignal = component.loadTagMessages(tag, self.tagContents?.index) { - updatedDisposable.set((historySignal - |> deliverOnMainQueue).startStrict(next: { [weak self] view in - guard let self else { - return - } - - let contentsId = self.nextContentsId - self.nextContentsId += 1 - self.contentsState = ContentsState( - id: contentsId, - tag: tag, - entries: view.entries.reversed().map { entry in - return EngineMessage(entry.message) - }, - hasEarlier: view.earlierId != nil, - hasLater: view.laterId != nil - ) - if !self.isUpdating { - self.state?.updated(transition: .immediate) - } - - if !self.didSetReady { - self.didSetReady = true - self.isReadyPromise.set(.single(true)) - } - })) + if let (currentIndex, disposable) = self.tagContents { + if let loadAroundIndex, loadAroundIndex != currentIndex { + switch component.contents { + case .empty: + break + case let .tag(tag): + disposable?.dispose() + let updatedDisposable = MetaDisposable() + self.tagContents = (loadAroundIndex, updatedDisposable) + + if let historySignal = component.loadTagMessages(tag, self.tagContents?.index) { + updatedDisposable.set((historySignal + |> deliverOnMainQueue).startStrict(next: { [weak self] view in + guard let self else { + return + } + + let contentsId = self.nextContentsId + self.nextContentsId += 1 + self.contentsState = ContentsState( + id: contentsId, + contentId: .tag(tag), + entries: view.entries.reversed().map { entry in + return EngineMessage(entry.message) + }, + hasEarlier: view.earlierId != nil, + hasLater: view.laterId != nil + ) + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + + if !self.didSetReady { + self.didSetReady = true + self.isReadyPromise.set(.single(true)) + } + })) + } + case .search: + break } } } } switch component.contents { + case .empty: + if previousComponent?.contents != component.contents { + self.tagContents?.disposable?.dispose() + self.tagContents = nil + + self.searchContents?.disposable?.dispose() + self.searchContents = nil + + let contentsId = self.nextContentsId + self.nextContentsId += 1 + self.contentsState = ContentsState( + id: contentsId, + contentId: .empty, + entries: [], + hasEarlier: false, + hasLater: false + ) + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + + if !self.didSetReady { + self.didSetReady = true + self.isReadyPromise.set(.single(true)) + } + } case let .tag(tag): if previousComponent?.contents != component.contents { self.tagContents?.disposable?.dispose() + self.tagContents = nil + + self.searchContents?.disposable?.dispose() + self.searchContents = nil let disposable = MetaDisposable() self.tagContents = (nil, disposable) @@ -303,7 +350,7 @@ public final class ChatInlineSearchResultsListComponent: Component { self.nextContentsId += 1 self.contentsState = ContentsState( id: contentsId, - tag: tag, + contentId: .tag(tag), entries: view.entries.reversed().map { entry in return EngineMessage(entry.message) }, @@ -314,6 +361,46 @@ public final class ChatInlineSearchResultsListComponent: Component { self.state?.updated(transition: .immediate) } + if !self.didSetReady { + self.didSetReady = true + self.isReadyPromise.set(.single(true)) + } + })) + } + } + case .search: + if previousComponent?.contents != component.contents { + self.tagContents?.disposable?.dispose() + self.tagContents = nil + + self.searchContents?.disposable?.dispose() + self.searchContents = nil + + let disposable = MetaDisposable() + self.searchContents = (nil, disposable) + + if let historySignal = component.getSearchResult() { + disposable.set((historySignal + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self else { + return + } + + let contentsId = self.nextContentsId + self.nextContentsId += 1 + self.contentsState = ContentsState( + id: contentsId, + contentId: .search, + entries: result?.messages.map { entry in + return EngineMessage(entry) + } ?? [], + hasEarlier: false, + hasLater: false + ) + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + if !self.didSetReady { self.didSetReady = true self.isReadyPromise.set(.single(true)) @@ -521,7 +608,7 @@ public final class ChatInlineSearchResultsListComponent: Component { } var scrollToItem: ListViewScrollToItem? - if previousContentsState?.tag != contentsState.tag && !contentsState.entries.isEmpty { + if previousContentsState?.contentId != contentsState.contentId && !contentsState.entries.isEmpty { scrollToItem = ListViewScrollToItem( index: 0, position: .top(0.0), diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift index 9678cd3744..80df1b1ffd 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift @@ -17,6 +17,83 @@ import ChatListUI import DeleteChatPeerActionSheetItem import UndoUI +private final class SearchNavigationContentNode: ASDisplayNode, PeerInfoPanelNodeNavigationContentNode { + private struct Params: Equatable { + var width: CGFloat + var defaultHeight: CGFloat + var insets: UIEdgeInsets + + init(width: CGFloat, defaultHeight: CGFloat, insets: UIEdgeInsets) { + self.width = width + self.defaultHeight = defaultHeight + self.insets = insets + } + } + + weak var chatController: ChatController? + let contentNode: NavigationBarContentNode + + var panelNode: ChatControllerCustomNavigationPanelNode? + private var appliedPanelNode: ChatControllerCustomNavigationPanelNode? + + private var params: Params? + + init(chatController: ChatController, contentNode: NavigationBarContentNode) { + self.chatController = chatController + self.contentNode = contentNode + + super.init() + + self.addSubnode(self.contentNode) + } + + func update(transition: ContainedViewLayoutTransition) { + if let params = self.params { + let _ = self.update(width: params.width, defaultHeight: params.defaultHeight, insets: params.insets, transition: transition) + } + } + + func update(width: CGFloat, defaultHeight: CGFloat, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) -> CGFloat { + self.params = Params(width: width, defaultHeight: defaultHeight, insets: insets) + + let size = CGSize(width: width, height: defaultHeight) + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 10.0), size: size)) + self.contentNode.updateLayout(size: size, leftInset: insets.left, rightInset: insets.right, transition: transition) + + var contentHeight: CGFloat = size.height + 10.0 + + if self.appliedPanelNode !== self.panelNode { + if let previous = self.appliedPanelNode { + transition.updateAlpha(node: previous, alpha: 0.0, completion: { [weak previous] _ in + previous?.removeFromSupernode() + }) + } + + self.appliedPanelNode = self.panelNode + if let panelNode = self.panelNode, let chatController = self.chatController { + self.addSubnode(panelNode) + let panelLayout = panelNode.updateLayout(width: width, leftInset: insets.left, rightInset: insets.right, transition: .immediate, chatController: chatController) + let panelHeight = panelLayout.backgroundHeight + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: panelHeight)) + panelNode.frame = panelFrame + panelNode.alpha = 0.0 + transition.updateAlpha(node: panelNode, alpha: 1.0) + + contentHeight += panelHeight - 1.0 + } + } else if let panelNode = self.panelNode, let chatController = self.chatController { + let panelLayout = panelNode.updateLayout(width: width, leftInset: insets.left, rightInset: insets.right, transition: transition, chatController: chatController) + let panelHeight = panelLayout.backgroundHeight + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: panelHeight)) + transition.updateFrame(node: panelNode, frame: panelFrame) + + contentHeight += panelHeight - 1.0 + } + + return contentHeight + } +} + public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { private let context: AccountContext @@ -24,7 +101,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI public weak var parentController: ViewController? - private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)? private let ready = Promise() private var didSetReady: Bool = false @@ -50,6 +127,16 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI private var emptyShimmerEffectNode: ChatListShimmerNode? private var shimmerNodeOffset: CGFloat = 0.0 private var floatingHeaderOffset: CGFloat? + + private let coveringView: UIView + private var chatController: ChatController? + private var removeChatWhenNotSearching: Bool = false + + private var searchNavigationContentNode: SearchNavigationContentNode? + public var navigationContentNode: PeerInfoPanelNodeNavigationContentNode? { + return self.searchNavigationContentNode + } + public var externalDataUpdated: ((ContainedViewLayoutTransition) -> Void)? public init(context: AccountContext, navigationController: @escaping () -> NavigationController?) { self.context = context @@ -58,6 +145,8 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI self.presentationData = presentationData let strings = presentationData.strings + self.coveringView = UIView() + self.chatListNode = ChatListNode( context: self.context, location: .savedMessagesChats, @@ -83,8 +172,12 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI super.init() + self.clipsToBounds = true + self.addSubnode(self.chatListNode) + self.view.addSubview(self.coveringView) + self.presentationDataDisposable = (self.context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in guard let self else { @@ -293,12 +386,89 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI deinit { self.presentationDataDisposable?.dispose() } + + public func activateSearch() { + if self.chatController == nil { + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: self.context.account.peerId), subject: nil, botStart: nil, mode: .standard(.embedded(invertDirection: false))) + chatController.alwaysShowSearchResultsAsList = true + + self.chatController = chatController + chatController.navigation_setNavigationController(self.navigationController()) + + self.insertSubnode(chatController.displayNode, aboveSubnode: self.chatListNode) + chatController.displayNode.alpha = 0.0 + chatController.displayNode.clipsToBounds = true + + self.updateChatController(transition: .immediate) + + let _ = (chatController.ready.get() + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self, weak chatController] _ in + guard let self, let chatController, self.chatController === chatController else { + return + } + + chatController.stateUpdated = { [weak self] transition in + guard let self, let chatController = self.chatController else { + return + } + if let contentNode = chatController.customNavigationBarContentNode { + self.removeChatWhenNotSearching = true + + chatController.displayNode.layer.allowsGroupOpacity = true + if transition.isAnimated { + Transition.easeInOut(duration: 0.2).setAlpha(layer: chatController.displayNode.layer, alpha: 1.0) + } + + if self.searchNavigationContentNode?.contentNode !== contentNode { + self.searchNavigationContentNode = SearchNavigationContentNode(chatController: chatController, contentNode: contentNode) + self.searchNavigationContentNode?.panelNode = chatController.customNavigationPanelNode + self.externalDataUpdated?(transition) + } else if self.searchNavigationContentNode?.panelNode !== chatController.customNavigationPanelNode { + self.searchNavigationContentNode?.panelNode = chatController.customNavigationPanelNode + self.externalDataUpdated?(transition.isAnimated ? transition : .animated(duration: 0.4, curve: .spring)) + } else { + self.searchNavigationContentNode?.update(transition: transition) + } + } else { + if self.searchNavigationContentNode !== nil { + self.searchNavigationContentNode = nil + self.externalDataUpdated?(transition) + } + + if self.removeChatWhenNotSearching { + self.removeChatController() + } + } + } + + chatController.activateSearch(domain: .everything, query: "") + }) + } + } + + private func removeChatController() { + if let chatController = self.chatController { + self.chatController = nil + + let displayNode = chatController.displayNode + chatController.displayNode.layer.allowsGroupOpacity = true + chatController.displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak displayNode] _ in + displayNode?.removeFromSupernode() + }) + } + } public func ensureMessageIsVisible(id: MessageId) { } public func scrollToTop() -> Bool { - self.chatListNode.scrollToPosition(.top(adjustForTempInset: false)) + if let chatController = self.chatController { + let _ = chatController.performScrollToTop() + } else { + self.chatListNode.scrollToPosition(.top(adjustForTempInset: false)) + } return false } @@ -356,25 +526,67 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UI public func updateSelectedMessages(animated: Bool) { } - public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) + private func updateChatController(transition: ContainedViewLayoutTransition) { + guard let chatController = self.chatController else { + return + } + guard let currentParams = self.currentParams else { + return + } - transition.updateFrame(node: self.chatListNode, frame: CGRect(origin: CGPoint(), size: size)) + let size = currentParams.size + let topInset = currentParams.topInset + let sideInset = currentParams.sideInset + let bottomInset = currentParams.bottomInset + let navigationHeight = currentParams.navigationHeight + let deviceMetrics = currentParams.deviceMetrics + let isScrollingLockedAtTop = currentParams.isScrollingLockedAtTop + + let fullHeight = navigationHeight + size.height + + let chatFrame = CGRect(origin: CGPoint(x: 0.0, y: -navigationHeight), size: CGSize(width: size.width, height: fullHeight)) + + if !chatController.displayNode.bounds.isEmpty { + if let contextController = chatController.visibleContextController as? ContextController { + let deltaY = chatFrame.minY - chatController.displayNode.frame.minY + contextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -deltaY * 0.0), transition: transition) + } + } + + let combinedBottomInset = bottomInset + transition.updateFrame(node: chatController.displayNode, frame: chatFrame) + chatController.updateIsScrollingLockedAtTop(isScrollingLockedAtTop: isScrollingLockedAtTop) + chatController.containerLayoutUpdated(ContainerViewLayout(size: chatFrame.size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: topInset + navigationHeight, left: sideInset, bottom: combinedBottomInset, right: sideInset), safeInsets: UIEdgeInsets(top: navigationHeight + topInset + 4.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + } + + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics: deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) + + self.coveringView.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + transition.updateFrame(view: self.coveringView, frame: CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: size.width, height: topInset + 1.0))) + + let fullHeight = navigationHeight + size.height + let chatFrame = CGRect(origin: CGPoint(x: 0.0, y: -navigationHeight), size: CGSize(width: size.width, height: fullHeight)) + let combinedBottomInset = bottomInset + + transition.updateFrame(node: self.chatListNode, frame: chatFrame) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.chatListNode.updateLayout( transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets( size: size, - insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), + insets: UIEdgeInsets(top: topInset + navigationHeight, left: sideInset, bottom: combinedBottomInset, right: sideInset), duration: duration, curve: curve ), - visibleTopInset: topInset, - originalTopInset: topInset, + visibleTopInset: topInset + navigationHeight, + originalTopInset: topInset + navigationHeight, storiesInset: 0.0, inlineNavigationLocation: nil, inlineNavigationTransitionFraction: 0.0 ) + + self.updateChatController(transition: transition) } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift index ba7225d538..5fa729736d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift @@ -150,6 +150,7 @@ public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro self.coveringView = UIView() self.chatController = context.sharedContext.makeChatController(context: context, chatLocation: .replyThread(message: ChatReplyThreadMessage(peerId: context.account.peerId, threadId: peerId.toInt64(), channelMessageId: nil, isChannelPost: false, isForumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)), subject: nil, botStart: nil, mode: .standard(.embedded(invertDirection: true))) + self.chatController.navigation_setNavigationController(navigationController()) super.init() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index e5ab3ec5f5..e0edabee79 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -103,6 +103,7 @@ import MediaPickerUI import AttachmentUI import BoostLevelIconComponent import PeerInfoChatPaneNode +import PeerInfoChatListPaneNode public enum PeerInfoAvatarEditingMode { case generic @@ -9289,6 +9290,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .savedMessages = currentPaneKey, let paneNode = self.paneContainerNode.currentPane?.node as? PeerInfoChatPaneNode { paneNode.activateSearch() return + } else if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .savedMessagesChats = currentPaneKey, let paneNode = self.paneContainerNode.currentPane?.node as? PeerInfoChatListPaneNode { + paneNode.activateSearch() + return } self.headerNode.navigationButtonContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) diff --git a/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift b/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift index ec3434059f..6c61b3ac2e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift @@ -149,19 +149,19 @@ private final class StoryQualityUpgradeSheetContentComponent: Component { let iconSize = self.icon.update( transition: transition, component: AnyComponent(LottieComponent( - content: LottieComponent.AppBundleContent(name: "ChatListNoResults"), + content: LottieComponent.AppBundleContent(name: "StoryUpgradeSheet"), color: nil, startingPosition: .begin, - size: CGSize(width: 120.0, height: 120.0) + size: CGSize(width: 100.0, height: 100.0) )), environment: {}, - containerSize: CGSize(width: 120.0, height: 120.0) + containerSize: CGSize(width: 100.0, height: 100.0) ) if let iconView = self.icon.view { if iconView.superview == nil { self.addSubview(iconView) } - transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 25.0), size: iconSize)) + transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 42.0), size: iconSize)) } contentHeight += 138.0 diff --git a/submodules/TelegramUI/Resources/Animations/StoryUpgradeSheet.tgs b/submodules/TelegramUI/Resources/Animations/StoryUpgradeSheet.tgs new file mode 100644 index 0000000000..d57e811cc8 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/StoryUpgradeSheet.tgs differ diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index 3bdee209cc..4ae3d58e7e 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -398,7 +398,7 @@ func updateChatPresentationInterfaceStateImpl( } if updatedChatPresentationInterfaceState.displayHistoryFilterAsList { - if updatedChatPresentationInterfaceState.search == nil || updatedChatPresentationInterfaceState.historyFilter == nil { + if updatedChatPresentationInterfaceState.search?.resultsState == nil && updatedChatPresentationInterfaceState.historyFilter == nil && !selfController.alwaysShowSearchResultsAsList { updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedDisplayHistoryFilterAsList(false) } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 1e37226b40..9cb9ce92a2 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -581,6 +581,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var performTextSelectionAction: ((Message?, Bool, NSAttributedString, TextSelectionAction) -> Void)? var performOpenURL: ((Message?, String, Promise?) -> Void)? + public var alwaysShowSearchResultsAsList: Bool = false { + didSet { + self.presentationInterfaceState = self.presentationInterfaceState.updatedDisplayHistoryFilterAsList(self.alwaysShowSearchResultsAsList) + self.chatDisplayNode.alwaysShowSearchResultsAsList = self.alwaysShowSearchResultsAsList + } + } + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(.default), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = []) { let _ = ChatControllerCount.modify { value in return value + 1 @@ -10958,11 +10965,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - if let search = self.presentationInterfaceState.search, !search.query.isEmpty { - self.interfaceInteraction?.openSearchResults() - return - } - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in return state.updatedDisplayHistoryFilterAsList(displayAsList) }) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 3fd70d1b20..c9bee43e42 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -135,6 +135,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let loadingNode: ChatLoadingNode private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode? + var alwaysShowSearchResultsAsList: Bool = false + private var skippedShowSearchResultsAsListAnimationOnce: Bool = false var inlineSearchResults: ComponentView? private var inlineSearchResultsReadyDisposable: Disposable? private var inlineSearchResultsReady: Bool = false @@ -2430,7 +2432,17 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.updatePlainInputSeparator(transition: transition) - if self.chatPresentationInterfaceState.displayHistoryFilterAsList, let peerId = self.chatPresentationInterfaceState.chatLocation.peerId, let historyFilter = self.chatPresentationInterfaceState.historyFilter { + var displayInlineSearch = false + if self.chatPresentationInterfaceState.displayHistoryFilterAsList { + if self.chatPresentationInterfaceState.historyFilter != nil || self.chatPresentationInterfaceState.search?.resultsState != nil { + displayInlineSearch = true + } + if self.alwaysShowSearchResultsAsList { + displayInlineSearch = true + } + } + + if let peerId = self.chatPresentationInterfaceState.chatLocation.peerId, displayInlineSearch { let inlineSearchResults: ComponentView var inlineSearchResultsTransition = Transition(transition) if let current = self.inlineSearchResults { @@ -2441,6 +2453,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inlineSearchResults = inlineSearchResults } + let mappedContents: ChatInlineSearchResultsListComponent.Contents + if let _ = self.chatPresentationInterfaceState.search?.resultsState { + mappedContents = .search + } else if let historyFilter = self.chatPresentationInterfaceState.historyFilter { + mappedContents = .tag(historyFilter.customTag) + } else { + mappedContents = .empty + } + let context = self.context let _ = inlineSearchResults.update( transition: inlineSearchResultsTransition, @@ -2455,7 +2476,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { nameDisplayOrder: self.chatPresentationInterfaceState.nameDisplayOrder ), peerId: peerId, - contents: .tag(historyFilter.customTag), + contents: mappedContents, insets: childContentInsets, messageSelected: { [weak self] message in guard let self else { @@ -2512,6 +2533,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return .single(view) } } + }, + getSearchResult: { [weak self] in + guard let self, let controller = self.controller else { + return nil + } + return controller.searchResult.get() + |> map { result in + return result?.0 + } } )), environment: {}, @@ -2521,7 +2551,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var animateIn = false if inlineSearchResultsView.superview == nil { animateIn = true - inlineSearchResultsView.alpha = 0.0 + if !self.alwaysShowSearchResultsAsList || self.skippedShowSearchResultsAsListAnimationOnce { + inlineSearchResultsView.alpha = 0.0 + } + self.skippedShowSearchResultsAsListAnimationOnce = true inlineSearchResultsView.layer.allowsGroupOpacity = true self.contentContainerNode.view.insertSubview(inlineSearchResultsView, aboveSubview: self.historyNodeContainer.view) } @@ -2538,11 +2571,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { guard let inlineSearchResultsView = self.inlineSearchResults?.view as? ChatInlineSearchResultsListComponent.View else { return } - inlineSearchResultsView.alpha = 1.0 - inlineSearchResultsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - inlineSearchResultsView.animateIn() + if inlineSearchResultsView.alpha == 0.0 { + inlineSearchResultsView.alpha = 1.0 - transition.updateSublayerTransformScale(node: self.historyNodeContainer, scale: CGPoint(x: 0.95, y: 0.95)) + inlineSearchResultsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + inlineSearchResultsView.animateIn() + + transition.updateSublayerTransformScale(node: self.historyNodeContainer, scale: CGPoint(x: 0.95, y: 0.95)) + } }) } } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift index d4d7b60a20..0d6dd32c6b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift @@ -39,6 +39,10 @@ extension ChatControllerImpl { c.dismiss() return } + + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in + return state.updatedDisplayHistoryFilterAsList(false) + }) c.dismiss() diff --git a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift index 40a403c964..96eaa3add0 100644 --- a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift @@ -168,12 +168,18 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { if let currentId = results.currentId, let index = results.messageIndices.firstIndex(where: { $0.id == currentId }) { canChangeListMode = true - let adjustedIndex = results.messageIndices.count - 1 - index - - //TODO:localize - resultsTextString.append(AnimatedTextComponent.Item(id: AnyHashable("search_n"), content: .number(adjustedIndex + 1, minDigits: 1))) - resultsTextString.append(AnimatedTextComponent.Item(id: AnyHashable("search_of"), isUnbreakable: true, content: .text(" of "))) - resultsTextString.append(AnimatedTextComponent.Item(id: AnyHashable("search_m"), content: .number(displayTotalCount, minDigits: 1))) + if params.interfaceState.displayHistoryFilterAsList { + //TODO:localize + resultsTextString.append(AnimatedTextComponent.Item(id: AnyHashable("count_n"), content: .number(displayTotalCount, minDigits: 1))) + resultsTextString.append(AnimatedTextComponent.Item(id: AnyHashable("count_message"), isUnbreakable: true, content: .text(displayTotalCount == 1 ? " message" : " messages"))) + } else { + let adjustedIndex = results.messageIndices.count - 1 - index + + //TODO:localize + resultsTextString.append(AnimatedTextComponent.Item(id: AnyHashable("search_n"), content: .number(adjustedIndex + 1, minDigits: 1))) + resultsTextString.append(AnimatedTextComponent.Item(id: AnyHashable("search_of"), isUnbreakable: true, content: .text(" of "))) + resultsTextString.append(AnimatedTextComponent.Item(id: AnyHashable("search_m"), content: .number(displayTotalCount, minDigits: 1))) + } } else { canChangeListMode = false