Add inline search results

This commit is contained in:
Isaac 2024-01-30 10:08:46 +01:00
parent 4ffc6367bb
commit d3bfd68386
13 changed files with 429 additions and 74 deletions

View File

@ -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)

View File

@ -23,6 +23,7 @@ swift_library(
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/UIKitRuntimeUtils",
"//submodules/ChatPresentationInterfaceState",
],
visibility = [
"//visibility:public",

View File

@ -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<MessageHistoryView, NoError>?
public let getSearchResult: () -> Signal<SearchMessagesResult?, NoError>?
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<MessageHistoryView, NoError>?
loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal<MessageHistoryView, NoError>?,
getSearchResult: @escaping () -> Signal<SearchMessagesResult?, NoError>?
) {
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),

View File

@ -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<Bool>()
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? {

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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)
}
}

View File

@ -581,6 +581,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var performTextSelectionAction: ((Message?, Bool, NSAttributedString, TextSelectionAction) -> Void)?
var performOpenURL: ((Message?, String, Promise<Bool>?) -> 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<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(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)
})

View File

@ -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<Empty>?
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<Empty>
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))
}
})
}
}

View File

@ -39,6 +39,10 @@ extension ChatControllerImpl {
c.dismiss()
return
}
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
return state.updatedDisplayHistoryFilterAsList(false)
})
c.dismiss()

View File

@ -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