mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
983 lines
46 KiB
Swift
983 lines
46 KiB
Swift
import UIKit
|
|
import ComponentFlow
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import TelegramCore
|
|
import Postbox
|
|
import AccountContext
|
|
import ChatListUI
|
|
import MergeLists
|
|
import ComponentDisplayAdapters
|
|
import TelegramPresentationData
|
|
import SwiftSignalKit
|
|
import TelegramUIPreferences
|
|
import UIKitRuntimeUtils
|
|
import ChatPresentationInterfaceState
|
|
import ContactsPeerItem
|
|
import ItemListUI
|
|
import ChatListSearchItemHeader
|
|
import LottieComponent
|
|
import MultilineTextComponent
|
|
|
|
public final class ChatInlineSearchResultsListComponent: Component {
|
|
public struct Presentation: Equatable {
|
|
public var theme: PresentationTheme
|
|
public var strings: PresentationStrings
|
|
public var chatListFontSize: PresentationFontSize
|
|
public var dateTimeFormat: PresentationDateTimeFormat
|
|
public var nameSortOrder: PresentationPersonNameOrder
|
|
public var nameDisplayOrder: PresentationPersonNameOrder
|
|
|
|
public init(
|
|
theme: PresentationTheme,
|
|
strings: PresentationStrings,
|
|
chatListFontSize: PresentationFontSize,
|
|
dateTimeFormat: PresentationDateTimeFormat,
|
|
nameSortOrder: PresentationPersonNameOrder,
|
|
nameDisplayOrder: PresentationPersonNameOrder
|
|
) {
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.chatListFontSize = chatListFontSize
|
|
self.dateTimeFormat = dateTimeFormat
|
|
self.nameSortOrder = nameSortOrder
|
|
self.nameDisplayOrder = nameDisplayOrder
|
|
}
|
|
|
|
public static func ==(lhs: Presentation, rhs: Presentation) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.strings != rhs.strings {
|
|
return false
|
|
}
|
|
if lhs.chatListFontSize != rhs.chatListFontSize {
|
|
return false
|
|
}
|
|
if lhs.dateTimeFormat != rhs.dateTimeFormat {
|
|
return false
|
|
}
|
|
if lhs.nameSortOrder != rhs.nameSortOrder {
|
|
return false
|
|
}
|
|
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
public enum Contents: Equatable {
|
|
case empty
|
|
case tag(MemoryBuffer)
|
|
case search(query: String, includeSavedPeers: Bool)
|
|
}
|
|
|
|
public let context: AccountContext
|
|
public let presentation: Presentation
|
|
public let peerId: EnginePeer.Id?
|
|
public let contents: Contents
|
|
public let insets: UIEdgeInsets
|
|
public let inputHeight: CGFloat
|
|
public let showEmptyResults: Bool
|
|
public let messageSelected: (EngineMessage) -> Void
|
|
public let peerSelected: (EnginePeer) -> Void
|
|
public let loadTagMessages: (MemoryBuffer, MessageIndex?) -> Signal<MessageHistoryView, NoError>?
|
|
public let getSearchResult: () -> Signal<SearchMessagesResult?, NoError>?
|
|
public let getSavedPeers: (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>?
|
|
public let loadMoreSearchResults: () -> Void
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
presentation: Presentation,
|
|
peerId: EnginePeer.Id?,
|
|
contents: Contents,
|
|
insets: UIEdgeInsets,
|
|
inputHeight: CGFloat,
|
|
showEmptyResults: Bool,
|
|
messageSelected: @escaping (EngineMessage) -> Void,
|
|
peerSelected: @escaping (EnginePeer) -> Void,
|
|
loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal<MessageHistoryView, NoError>?,
|
|
getSearchResult: @escaping () -> Signal<SearchMessagesResult?, NoError>?,
|
|
getSavedPeers: @escaping (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>?,
|
|
loadMoreSearchResults: @escaping () -> Void
|
|
) {
|
|
self.context = context
|
|
self.presentation = presentation
|
|
self.peerId = peerId
|
|
self.contents = contents
|
|
self.insets = insets
|
|
self.inputHeight = inputHeight
|
|
self.showEmptyResults = showEmptyResults
|
|
self.messageSelected = messageSelected
|
|
self.peerSelected = peerSelected
|
|
self.loadTagMessages = loadTagMessages
|
|
self.getSearchResult = getSearchResult
|
|
self.getSavedPeers = getSavedPeers
|
|
self.loadMoreSearchResults = loadMoreSearchResults
|
|
}
|
|
|
|
public static func ==(lhs: ChatInlineSearchResultsListComponent, rhs: ChatInlineSearchResultsListComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.presentation != rhs.presentation {
|
|
return false
|
|
}
|
|
if lhs.peerId != rhs.peerId {
|
|
return false
|
|
}
|
|
if lhs.contents != rhs.contents {
|
|
return false
|
|
}
|
|
if lhs.insets != rhs.insets {
|
|
return false
|
|
}
|
|
if lhs.inputHeight != rhs.inputHeight {
|
|
return false
|
|
}
|
|
if lhs.showEmptyResults != rhs.showEmptyResults {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private enum Entry: Equatable, Comparable {
|
|
enum Id: Hashable {
|
|
case peer(EnginePeer.Id)
|
|
case message(EngineMessage.Id)
|
|
}
|
|
|
|
case peer(EnginePeer)
|
|
case message(EngineMessage)
|
|
|
|
var id: Id {
|
|
switch self {
|
|
case let .peer(peer):
|
|
return .peer(peer.id)
|
|
case let .message(message):
|
|
return .message(message.id)
|
|
}
|
|
}
|
|
|
|
static func ==(lhs: Entry, rhs: Entry) -> Bool {
|
|
switch lhs {
|
|
case let .peer(peer):
|
|
if case .peer(peer) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .message(message):
|
|
if case .message(message) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
static func <(lhs: Entry, rhs: Entry) -> Bool {
|
|
switch lhs {
|
|
case let .peer(lhsPeer):
|
|
switch rhs {
|
|
case let .peer(rhsPeer):
|
|
if lhsPeer.debugDisplayTitle != rhsPeer.debugDisplayTitle {
|
|
return lhsPeer.debugDisplayTitle < rhsPeer.debugDisplayTitle
|
|
}
|
|
return lhsPeer.id < rhsPeer.id
|
|
case .message:
|
|
return true
|
|
}
|
|
case let .message(lhsMessage):
|
|
switch rhs {
|
|
case .peer:
|
|
return false
|
|
case let .message(rhsMessage):
|
|
return lhsMessage.index > rhsMessage.index
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ContentsState: Equatable {
|
|
enum ContentId: Equatable {
|
|
case empty
|
|
case tag(MemoryBuffer)
|
|
case search(String)
|
|
}
|
|
|
|
var id: Int
|
|
var contentId: ContentId
|
|
var entries: [Entry]
|
|
var messages: [EngineMessage]
|
|
var hasEarlier: Bool
|
|
var hasLater: Bool
|
|
|
|
init(id: Int, contentId: ContentId, entries: [Entry], messages: [EngineMessage], hasEarlier: Bool, hasLater: Bool) {
|
|
self.id = id
|
|
self.contentId = contentId
|
|
self.entries = entries
|
|
self.messages = messages
|
|
self.hasEarlier = hasEarlier
|
|
self.hasLater = hasLater
|
|
}
|
|
}
|
|
|
|
public final class View: UIView {
|
|
private var component: ChatInlineSearchResultsListComponent?
|
|
private weak var state: EmptyComponentState?
|
|
private var isUpdating: Bool = false
|
|
|
|
private let listNode: ListView
|
|
private let emptyResultsTitle = ComponentView<Empty>()
|
|
private let emptyResultsText = ComponentView<Empty>()
|
|
private let emptyResultsAnimation = ComponentView<Empty>()
|
|
|
|
private var tagContents: (index: MessageIndex?, disposable: Disposable?)?
|
|
private var searchContents: (index: MessageIndex?, disposable: Disposable?)?
|
|
|
|
private var nextContentsId: Int = 0
|
|
private var contentsState: ContentsState?
|
|
private var appliedContentsState: ContentsState?
|
|
|
|
private var currentChatListPresentationData: (Presentation, ChatListPresentationData)?
|
|
private var chatListNodeInteraction: ChatListNodeInteraction?
|
|
|
|
private let isReadyPromise = Promise<Bool>()
|
|
private var didSetReady: Bool = false
|
|
public var isReady: Signal<Bool, NoError> {
|
|
return self.isReadyPromise.get()
|
|
}
|
|
|
|
override public init(frame: CGRect) {
|
|
self.listNode = ListView()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubnode(self.listNode)
|
|
|
|
self.listNode.beganInteractiveDragging = { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.window?.endEditing(true)
|
|
}
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.tagContents?.disposable?.dispose()
|
|
self.searchContents?.disposable?.dispose()
|
|
}
|
|
|
|
public func scrollToTop() {
|
|
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
}
|
|
|
|
public func animateIn() {
|
|
self.listNode.layer.animateSublayerScale(from: 0.95, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
|
|
|
if let blurFilter = makeBlurFilter() {
|
|
blurFilter.setValue(0.0 as NSNumber, forKey: "inputRadius")
|
|
self.listNode.layer.filters = [blurFilter]
|
|
self.listNode.layer.animate(from: 30.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak self] completed in
|
|
guard let self, completed else {
|
|
return
|
|
}
|
|
self.listNode.layer.filters = []
|
|
})
|
|
}
|
|
}
|
|
|
|
public func animateOut() {
|
|
self.listNode.layer.animateSublayerScale(from: 1.0, to: 0.95, duration: 0.3, removeOnCompletion: false)
|
|
|
|
if let blurFilter = makeBlurFilter() {
|
|
blurFilter.setValue(30.0 as NSNumber, forKey: "inputRadius")
|
|
self.listNode.layer.filters = [blurFilter]
|
|
self.listNode.layer.animate(from: 0.0 as NSNumber, to: 30.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: false)
|
|
}
|
|
}
|
|
|
|
func update(component: ChatInlineSearchResultsListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
|
self.isUpdating = true
|
|
defer {
|
|
self.isUpdating = false
|
|
}
|
|
|
|
let previousComponent = self.component
|
|
|
|
self.component = component
|
|
self.state = state
|
|
|
|
switch component.contents {
|
|
case .empty:
|
|
self.backgroundColor = nil
|
|
default:
|
|
break
|
|
}
|
|
|
|
self.listNode.frame = CGRect(origin: CGPoint(), size: availableSize)
|
|
let (listDuration, listCurve) = listViewAnimationDurationAndCurve(transition: transition.containedViewLayoutTransition)
|
|
self.listNode.transaction(
|
|
deleteIndices: [],
|
|
insertIndicesAndItems: [],
|
|
updateIndicesAndItems: [],
|
|
options: [.Synchronous, .LowLatency, .PreferSynchronousDrawing, .PreferSynchronousResourceLoading],
|
|
updateSizeAndInsets: ListViewUpdateSizeAndInsets(
|
|
size: availableSize,
|
|
insets: component.insets,
|
|
duration: listDuration,
|
|
curve: listCurve
|
|
),
|
|
updateOpaqueState: nil
|
|
)
|
|
|
|
self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in
|
|
guard let self else {
|
|
return
|
|
}
|
|
guard let stateId = opaqueTransactionState as? Int else {
|
|
return
|
|
}
|
|
guard let contentsState = self.contentsState, contentsState.id == stateId else {
|
|
return
|
|
}
|
|
guard let visibleRange = displayedRange.visibleRange else {
|
|
return
|
|
}
|
|
var loadAroundIndex: MessageIndex?
|
|
if visibleRange.firstIndex <= 5 {
|
|
if contentsState.hasLater {
|
|
loadAroundIndex = contentsState.messages.first?.index
|
|
}
|
|
} else if visibleRange.lastIndex >= contentsState.messages.count - 5 {
|
|
if contentsState.hasEarlier {
|
|
loadAroundIndex = contentsState.messages.last?.index
|
|
}
|
|
}
|
|
|
|
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 messages = view.entries.reversed().map { entry in
|
|
return EngineMessage(entry.message)
|
|
}
|
|
|
|
let contentsId = self.nextContentsId
|
|
self.nextContentsId += 1
|
|
self.contentsState = ContentsState(
|
|
id: contentsId,
|
|
contentId: .tag(tag),
|
|
entries: messages.map { message in
|
|
return .message(message)
|
|
},
|
|
messages: messages,
|
|
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
|
|
}
|
|
}
|
|
} else if let (currentIndex, disposable) = self.searchContents {
|
|
if let loadAroundIndex, loadAroundIndex != currentIndex {
|
|
switch component.contents {
|
|
case .empty:
|
|
break
|
|
case .tag:
|
|
break
|
|
case .search:
|
|
self.searchContents = (loadAroundIndex, disposable)
|
|
|
|
component.loadMoreSearchResults()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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: [],
|
|
messages: [],
|
|
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)
|
|
|
|
if let historySignal = component.loadTagMessages(tag, self.tagContents?.index) {
|
|
disposable.set((historySignal
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] view in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
let messages = view.entries.reversed().map { entry in
|
|
return EngineMessage(entry.message)
|
|
}
|
|
|
|
let contentsId = self.nextContentsId
|
|
self.nextContentsId += 1
|
|
self.contentsState = ContentsState(
|
|
id: contentsId,
|
|
contentId: .tag(tag),
|
|
entries: messages.map { message in
|
|
return .message(message)
|
|
},
|
|
messages: messages,
|
|
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 let .search(query, includeSavedPeers):
|
|
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)
|
|
|
|
let savedPeers: Signal<[(EnginePeer, MessageIndex?)], NoError>
|
|
if includeSavedPeers, !query.isEmpty, let savedPeersSignal = component.getSavedPeers(query) {
|
|
savedPeers = savedPeersSignal
|
|
} else {
|
|
savedPeers = .single([])
|
|
}
|
|
|
|
if let historySignal = component.getSearchResult() {
|
|
disposable.set((savedPeers
|
|
|> mapToSignal { savedPeers -> Signal<([(EnginePeer, MessageIndex?)], SearchMessagesResult?), NoError> in
|
|
if savedPeers.isEmpty {
|
|
return historySignal
|
|
|> map { result in
|
|
return ([], result)
|
|
}
|
|
} else {
|
|
return (.single(nil) |> then(historySignal))
|
|
|> map { result in
|
|
return (savedPeers, result)
|
|
}
|
|
}
|
|
}
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] savedPeers, result in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
let messages: [EngineMessage] = result?.messages.map { entry in
|
|
return EngineMessage(entry)
|
|
} ?? []
|
|
|
|
var entries: [Entry] = []
|
|
for (peer, _) in savedPeers {
|
|
entries.append(.peer(peer))
|
|
}
|
|
for message in messages {
|
|
entries.append(.message(message))
|
|
}
|
|
entries.sort()
|
|
|
|
let contentsId = self.nextContentsId
|
|
self.nextContentsId += 1
|
|
self.contentsState = ContentsState(
|
|
id: contentsId,
|
|
contentId: .search(query),
|
|
entries: entries,
|
|
messages: messages,
|
|
hasEarlier: !(result?.completed ?? true),
|
|
hasLater: false
|
|
)
|
|
if !self.isUpdating {
|
|
self.state?.updated(transition: .immediate)
|
|
}
|
|
|
|
if !self.didSetReady {
|
|
self.didSetReady = true
|
|
self.isReadyPromise.set(.single(true))
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
if let contentsState = self.contentsState, self.contentsState != self.appliedContentsState {
|
|
let previousContentsState = self.appliedContentsState
|
|
self.appliedContentsState = self.contentsState
|
|
|
|
let chatListNodeInteraction: ChatListNodeInteraction
|
|
if let current = self.chatListNodeInteraction {
|
|
chatListNodeInteraction = current
|
|
} else {
|
|
chatListNodeInteraction = ChatListNodeInteraction(
|
|
context: component.context,
|
|
animationCache: component.context.animationCache,
|
|
animationRenderer: component.context.animationRenderer,
|
|
activateSearch: {
|
|
},
|
|
peerSelected: { _, _, _, _ in
|
|
},
|
|
disabledPeerSelected: { _, _, _ in
|
|
},
|
|
togglePeerSelected: { _, _ in
|
|
},
|
|
togglePeersSelection: { _, _ in
|
|
},
|
|
additionalCategorySelected: { _ in
|
|
},
|
|
messageSelected: { [weak self] _, _, message, _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.listNode.clearHighlightAnimated(true)
|
|
|
|
self.component?.messageSelected(message)
|
|
},
|
|
groupSelected: { _ in
|
|
},
|
|
addContact: { _ in
|
|
},
|
|
setPeerIdWithRevealedOptions: { _, _ in
|
|
},
|
|
setItemPinned: { _, _ in
|
|
},
|
|
setPeerMuted: { _, _ in
|
|
},
|
|
setPeerThreadMuted: { _, _, _ in
|
|
},
|
|
deletePeer: { _, _ in
|
|
},
|
|
deletePeerThread: { _, _ in
|
|
},
|
|
setPeerThreadStopped: { _, _, _ in
|
|
},
|
|
setPeerThreadPinned: { _, _, _ in
|
|
},
|
|
setPeerThreadHidden: { _, _, _ in
|
|
},
|
|
updatePeerGrouping: { _, _ in
|
|
},
|
|
togglePeerMarkedUnread: { _, _ in
|
|
},
|
|
toggleArchivedFolderHiddenByDefault: {
|
|
},
|
|
toggleThreadsSelection: { _, _ in
|
|
},
|
|
hidePsa: { _ in
|
|
},
|
|
activateChatPreview: { item, _, node, gesture, _ in
|
|
gesture?.cancel()
|
|
},
|
|
present: { _ in
|
|
},
|
|
openForumThread: { _, _ in
|
|
},
|
|
openStorageManagement: {
|
|
},
|
|
openPasswordSetup: {
|
|
},
|
|
openPremiumIntro: {
|
|
},
|
|
openPremiumGift: { _ in
|
|
},
|
|
openPremiumManagement: {
|
|
},
|
|
openActiveSessions: {
|
|
},
|
|
openBirthdaySetup: {
|
|
},
|
|
performActiveSessionAction: { _, _ in
|
|
},
|
|
openChatFolderUpdates: {
|
|
},
|
|
hideChatFolderUpdates: {
|
|
},
|
|
openStories: { _, _ in
|
|
},
|
|
dismissNotice: { _ in
|
|
},
|
|
editPeer: { _ in
|
|
}
|
|
)
|
|
self.chatListNodeInteraction = chatListNodeInteraction
|
|
}
|
|
|
|
var searchTextHighightState: String?
|
|
if case let .search(query, _) = component.contents, !query.isEmpty {
|
|
searchTextHighightState = query.lowercased()
|
|
}
|
|
|
|
var allUpdated = false
|
|
if chatListNodeInteraction.searchTextHighightState != searchTextHighightState {
|
|
chatListNodeInteraction.searchTextHighightState = searchTextHighightState
|
|
allUpdated = true
|
|
}
|
|
|
|
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(
|
|
leftList: previousContentsState?.entries ?? [],
|
|
rightList: contentsState.entries,
|
|
isLess: { lhs, rhs in
|
|
return lhs < rhs
|
|
},
|
|
isEqual: { lhs, rhs in
|
|
return lhs == rhs
|
|
},
|
|
getId: { entry in
|
|
return entry.id
|
|
},
|
|
allUpdated: allUpdated
|
|
)
|
|
|
|
let displayMessagesHeader = contentsState.entries.count != contentsState.messages.count
|
|
|
|
let chatListPresentationData: ChatListPresentationData
|
|
if let current = self.currentChatListPresentationData, current.0 == component.presentation {
|
|
chatListPresentationData = current.1
|
|
} else {
|
|
chatListPresentationData = ChatListPresentationData(
|
|
theme: component.presentation.theme,
|
|
fontSize: component.presentation.chatListFontSize,
|
|
strings: component.presentation.strings,
|
|
dateTimeFormat: component.presentation.dateTimeFormat,
|
|
nameSortOrder: component.presentation.nameSortOrder,
|
|
nameDisplayOrder: component.presentation.nameDisplayOrder,
|
|
disableAnimations: false
|
|
)
|
|
self.currentChatListPresentationData = (component.presentation, chatListPresentationData)
|
|
}
|
|
|
|
let listPresentationData = ItemListPresentationData(component.context.sharedContext.currentPresentationData.with({ $0 }))
|
|
let peerSelected = component.peerSelected
|
|
|
|
let entryToItem: (Entry) -> ListViewItem = { entry -> ListViewItem in
|
|
switch entry {
|
|
case let .peer(peer):
|
|
return ContactsPeerItem(
|
|
presentationData: listPresentationData,
|
|
sortOrder: component.presentation.nameSortOrder,
|
|
displayOrder: component.presentation.nameDisplayOrder,
|
|
context: component.context,
|
|
peerMode: .generalSearch(isSavedMessages: true),
|
|
peer: .peer(peer: peer, chatPeer: peer),
|
|
status: .none,
|
|
badge: nil,
|
|
requiresPremiumForMessaging: false,
|
|
enabled: true,
|
|
selection: .none,
|
|
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
|
|
index: nil,
|
|
header: displayMessagesHeader ? ChatListSearchItemHeader(type: .chats, theme: listPresentationData.theme, strings: listPresentationData.strings) : nil,
|
|
action: { [weak self] peer in
|
|
self?.listNode.clearHighlightAnimated(true)
|
|
|
|
if case let .peer(peer?, _) = peer {
|
|
peerSelected(peer)
|
|
}
|
|
},
|
|
animationCache: component.context.animationCache,
|
|
animationRenderer: component.context.animationRenderer
|
|
)
|
|
case let .message(message):
|
|
var effectiveAuthor: EnginePeer?
|
|
|
|
if let forwardInfo = message.forwardInfo {
|
|
effectiveAuthor = forwardInfo.author.flatMap(EnginePeer.init)
|
|
if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature {
|
|
effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil))
|
|
}
|
|
}
|
|
if let sourceAuthorInfo = message._asMessage().sourceAuthorInfo {
|
|
if let originalAuthor = sourceAuthorInfo.originalAuthor, let peer = message.peers[originalAuthor] {
|
|
effectiveAuthor = EnginePeer(peer)
|
|
} else if let authorSignature = sourceAuthorInfo.originalAuthorName {
|
|
effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil))
|
|
}
|
|
}
|
|
if effectiveAuthor == nil {
|
|
effectiveAuthor = message.author
|
|
}
|
|
|
|
let renderedPeer: EngineRenderedPeer
|
|
if let effectiveAuthor, !component.showEmptyResults {
|
|
renderedPeer = EngineRenderedPeer(peer: effectiveAuthor)
|
|
} else {
|
|
var peers: [EnginePeer.Id: EnginePeer] = [:]
|
|
if let peer = message.peers[message.id.peerId] {
|
|
peers[message.id.peerId] = EnginePeer(peer)
|
|
}
|
|
renderedPeer = EngineRenderedPeer(peerId: message.id.peerId, peers: peers, associatedMedia: [:])
|
|
}
|
|
|
|
return ChatListItem(
|
|
presentationData: chatListPresentationData,
|
|
context: component.context,
|
|
chatListLocation: component.peerId == component.context.account.peerId ? .savedMessagesChats : .chatList(groupId: .root),
|
|
filterData: nil,
|
|
index: .forum(
|
|
pinnedIndex: .none,
|
|
timestamp: message.timestamp,
|
|
threadId: message.threadId ?? component.context.account.peerId.toInt64(),
|
|
namespace: message.id.namespace,
|
|
id: message.id.id
|
|
),
|
|
content: .peer(ChatListItemContent.PeerData(
|
|
messages: [message],
|
|
peer: renderedPeer,
|
|
threadInfo: nil,
|
|
combinedReadState: nil,
|
|
isRemovedFromTotalUnreadCount: false,
|
|
presence: nil,
|
|
hasUnseenMentions: false,
|
|
hasUnseenReactions: false,
|
|
draftState: nil,
|
|
mediaDraftContentType: nil,
|
|
inputActivities: nil,
|
|
promoInfo: nil,
|
|
ignoreUnreadBadge: false,
|
|
displayAsMessage: component.peerId != component.context.account.peerId && !component.showEmptyResults,
|
|
hasFailedMessages: false,
|
|
forumTopicData: nil,
|
|
topForumTopicItems: [],
|
|
autoremoveTimeout: nil,
|
|
storyState: nil,
|
|
requiresPremiumForMessaging: false,
|
|
displayAsTopicList: false,
|
|
tags: []
|
|
)),
|
|
editing: false,
|
|
hasActiveRevealControls: false,
|
|
selected: false,
|
|
header: displayMessagesHeader ? ChatListSearchItemHeader(type: .messages(location: nil), theme: listPresentationData.theme, strings: listPresentationData.strings) : nil,
|
|
enableContextActions: false,
|
|
hiddenOffset: false,
|
|
interaction: chatListNodeInteraction
|
|
)
|
|
}
|
|
}
|
|
|
|
var scrollToItem: ListViewScrollToItem?
|
|
if previousContentsState?.contentId != contentsState.contentId && !contentsState.entries.isEmpty {
|
|
scrollToItem = ListViewScrollToItem(
|
|
index: 0,
|
|
position: .top(0.0),
|
|
animated: false,
|
|
curve: .Default(duration: nil),
|
|
directionHint: .Up
|
|
)
|
|
}
|
|
|
|
self.listNode.transaction(
|
|
deleteIndices: deleteIndices.map { index in
|
|
return ListViewDeleteItem(index: index, directionHint: nil)
|
|
},
|
|
insertIndicesAndItems: indicesAndItems.map { index, item, previousIndex in
|
|
return ListViewInsertItem(
|
|
index: index,
|
|
previousIndex: previousIndex,
|
|
item: entryToItem(item),
|
|
directionHint: nil,
|
|
forceAnimateInsertion: false
|
|
)
|
|
},
|
|
updateIndicesAndItems: updateIndices.map { index, item, previousIndex in
|
|
return ListViewUpdateItem(
|
|
index: index,
|
|
previousIndex: previousIndex,
|
|
item: entryToItem(item),
|
|
directionHint: nil
|
|
)
|
|
},
|
|
options: [.Synchronous, .LowLatency, .PreferSynchronousDrawing, .PreferSynchronousResourceLoading],
|
|
scrollToItem: scrollToItem,
|
|
updateSizeAndInsets: nil,
|
|
updateOpaqueState: contentsState.id
|
|
)
|
|
|
|
switch component.contents {
|
|
case .empty:
|
|
self.backgroundColor = nil
|
|
default:
|
|
self.backgroundColor = component.presentation.theme.list.plainBackgroundColor
|
|
}
|
|
}
|
|
|
|
let fadeTransition = Transition.easeInOut(duration: 0.25)
|
|
if component.showEmptyResults, let appliedContentsState = self.appliedContentsState, appliedContentsState.entries.isEmpty, case let .search(query, _) = component.contents, !query.isEmpty {
|
|
let sideInset: CGFloat = 44.0
|
|
let emptyAnimationHeight = 148.0
|
|
let topInset: CGFloat = component.insets.top
|
|
let bottomInset: CGFloat = max(component.insets.bottom, component.inputHeight)
|
|
let visibleHeight = availableSize.height
|
|
let emptyAnimationSpacing: CGFloat = 8.0
|
|
let emptyTextSpacing: CGFloat = 8.0
|
|
|
|
let emptyResultsTitleSize = self.emptyResultsTitle.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.presentation.strings.HashtagSearch_NoResults, font: Font.semibold(17.0), textColor: component.presentation.theme.list.itemSecondaryTextColor)),
|
|
horizontalAlignment: .center
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
let emptyResultsTextSize = self.emptyResultsText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.presentation.strings.HashtagSearch_NoResultsQueryDescription(query).string, font: Font.regular(15.0), textColor: component.presentation.theme.list.itemSecondaryTextColor)),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 0
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
|
|
)
|
|
let emptyResultsAnimationSize = self.emptyResultsAnimation.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(LottieComponent(
|
|
content: LottieComponent.AppBundleContent(name: "ChatListNoResults")
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight)
|
|
)
|
|
|
|
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsTextSize.height + emptyTextSpacing
|
|
let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0)
|
|
|
|
let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize)
|
|
|
|
let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize)
|
|
|
|
let emptyResultsTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsTextSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsTextSize)
|
|
|
|
if let view = self.emptyResultsAnimation.view as? LottieComponent.View {
|
|
if view.superview == nil {
|
|
view.alpha = 0.0
|
|
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
|
self.addSubview(view)
|
|
view.playOnce()
|
|
}
|
|
view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size)
|
|
transition.setPosition(view: view, position: emptyResultsAnimationFrame.center)
|
|
}
|
|
if let view = self.emptyResultsTitle.view {
|
|
if view.superview == nil {
|
|
view.alpha = 0.0
|
|
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
|
self.addSubview(view)
|
|
}
|
|
view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size)
|
|
transition.setPosition(view: view, position: emptyResultsTitleFrame.center)
|
|
}
|
|
if let view = self.emptyResultsText.view {
|
|
if view.superview == nil {
|
|
view.alpha = 0.0
|
|
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
|
self.addSubview(view)
|
|
}
|
|
view.bounds = CGRect(origin: .zero, size: emptyResultsTextFrame.size)
|
|
transition.setPosition(view: view, position: emptyResultsTextFrame.center)
|
|
}
|
|
} else {
|
|
if let view = self.emptyResultsAnimation.view {
|
|
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
|
view.removeFromSuperview()
|
|
})
|
|
}
|
|
if let view = self.emptyResultsTitle.view {
|
|
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
|
view.removeFromSuperview()
|
|
})
|
|
}
|
|
if let view = self.emptyResultsText.view {
|
|
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
|
view.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|