mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Add inline search results
This commit is contained in:
parent
4ffc6367bb
commit
d3bfd68386
@ -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)
|
||||
|
@ -23,6 +23,7 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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),
|
||||
|
@ -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? {
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
BIN
submodules/TelegramUI/Resources/Animations/StoryUpgradeSheet.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/StoryUpgradeSheet.tgs
Normal file
Binary file not shown.
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,10 @@ extension ChatControllerImpl {
|
||||
c.dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
|
||||
return state.updatedDisplayHistoryFilterAsList(false)
|
||||
})
|
||||
|
||||
c.dismiss()
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user