mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 14:45:21 +00:00
Refactor PeerInfoUI and related modules
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import MergeLists
|
||||
import AccountContext
|
||||
import SearchUI
|
||||
import ContactsPeerItem
|
||||
|
||||
private enum ChannelDiscussionGroupSearchContent: Equatable {
|
||||
case peer(Peer)
|
||||
|
||||
static func ==(lhs: ChannelDiscussionGroupSearchContent, rhs: ChannelDiscussionGroupSearchContent) -> Bool {
|
||||
switch lhs {
|
||||
case let .peer(lhsPeer):
|
||||
if case let .peer(rhsPeer) = rhs {
|
||||
return lhsPeer.isEqual(rhsPeer)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var peerId: PeerId {
|
||||
switch self {
|
||||
case let .peer(peer):
|
||||
return peer.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChannelDiscussionGroupSearchInteraction {
|
||||
let peerSelected: (Peer) -> Void
|
||||
|
||||
init(peerSelected: @escaping (Peer) -> Void) {
|
||||
self.peerSelected = peerSelected
|
||||
}
|
||||
}
|
||||
|
||||
private struct ChannelDiscussionGroupSearchEntryId: Hashable {
|
||||
let peerId: PeerId
|
||||
}
|
||||
|
||||
private final class ChannelDiscussionGroupSearchEntry: Comparable, Identifiable {
|
||||
let index: Int
|
||||
let content: ChannelDiscussionGroupSearchContent
|
||||
|
||||
init(index: Int, content: ChannelDiscussionGroupSearchContent) {
|
||||
self.index = index
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var stableId: ChannelDiscussionGroupSearchEntryId {
|
||||
return ChannelDiscussionGroupSearchEntryId(peerId: self.content.peerId)
|
||||
}
|
||||
|
||||
static func ==(lhs: ChannelDiscussionGroupSearchEntry, rhs: ChannelDiscussionGroupSearchEntry) -> Bool {
|
||||
return lhs.index == rhs.index && lhs.content == rhs.content
|
||||
}
|
||||
|
||||
static func <(lhs: ChannelDiscussionGroupSearchEntry, rhs: ChannelDiscussionGroupSearchEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ChannelDiscussionGroupSearchInteraction) -> ListViewItem {
|
||||
switch self.content {
|
||||
case let .peer(peer):
|
||||
return ContactsPeerItem(theme: theme, strings: strings, sortOrder: .firstLast, displayOrder: .firstLast, account: account, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in
|
||||
interaction.peerSelected(peer)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelDiscussionGroupSearchContainerTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
let isSearching: Bool
|
||||
}
|
||||
|
||||
private func channelDiscussionGroupSearchContainerPreparedRecentTransition(from fromEntries: [ChannelDiscussionGroupSearchEntry], to toEntries: [ChannelDiscussionGroupSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ChannelDiscussionGroupSearchInteraction) -> ChannelDiscussionGroupSearchContainerTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) }
|
||||
|
||||
return ChannelDiscussionGroupSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
|
||||
}
|
||||
|
||||
private struct ChannelDiscussionGroupSearchContainerState: Equatable {
|
||||
}
|
||||
|
||||
final class ChannelDiscussionGroupSearchContainerNode: SearchDisplayControllerContentNode {
|
||||
private let context: AccountContext
|
||||
private let openPeer: (Peer) -> Void
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
private let listNode: ListView
|
||||
|
||||
private var enqueuedTransitions: [(ChannelDiscussionGroupSearchContainerTransition, Bool)] = []
|
||||
private var hasValidLayout = false
|
||||
|
||||
private let searchQuery = Promise<String?>()
|
||||
private let searchDisposable = MetaDisposable()
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PresentationDateTimeFormat)>
|
||||
|
||||
init(context: AccountContext, peers: [Peer], openPeer: @escaping (Peer) -> Void) {
|
||||
self.context = context
|
||||
self.openPeer = openPeer
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.nameSortOrder, self.presentationData.nameDisplayOrder, self.presentationData.dateTimeFormat))
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.listNode = ListView()
|
||||
|
||||
super.init()
|
||||
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
|
||||
self.listNode.isHidden = true
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
self.addSubnode(self.listNode)
|
||||
|
||||
/*let statePromise = ValuePromise(ChannelDiscussionGroupSearchContainerState(), ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: ChannelDiscussionGroupSearchContainerState())
|
||||
let updateState: ((ChannelDiscussionGroupSearchContainerState) -> ChannelDiscussionGroupSearchContainerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}*/
|
||||
|
||||
let interaction = ChannelDiscussionGroupSearchInteraction(peerSelected: { peer in
|
||||
openPeer(peer)
|
||||
})
|
||||
|
||||
var searchIndex: [ValueBoxKey: [Peer]] = [:]
|
||||
for peer in peers {
|
||||
for token in peer.indexName.indexTokens {
|
||||
if searchIndex[token] == nil {
|
||||
searchIndex[token] = []
|
||||
}
|
||||
searchIndex[token]!.append(peer)
|
||||
}
|
||||
}
|
||||
|
||||
let foundItems = searchQuery.get()
|
||||
|> mapToSignal { query -> Signal<[ChannelDiscussionGroupSearchEntry]?, NoError> in
|
||||
guard let query = query, !query.isEmpty else {
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
var entries: [ChannelDiscussionGroupSearchEntry] = []
|
||||
let searchQueryTokens = stringIndexTokens(query.lowercased(), transliteration: .none)
|
||||
var filteredPeers: [Peer] = []
|
||||
var existingPeers = Set<PeerId>()
|
||||
for (key, values) in searchIndex {
|
||||
inner: for token in searchQueryTokens {
|
||||
if token.isPrefix(to: key) {
|
||||
for peer in values {
|
||||
if !existingPeers.contains(peer.id) {
|
||||
existingPeers.insert(peer.id)
|
||||
filteredPeers.append(peer)
|
||||
}
|
||||
}
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
for peer in filteredPeers {
|
||||
entries.append(ChannelDiscussionGroupSearchEntry(index: entries.count, content: .peer(peer)))
|
||||
}
|
||||
return .single(entries)
|
||||
}
|
||||
|
||||
let previousSearchItems = Atomic<[ChannelDiscussionGroupSearchEntry]?>(value: nil)
|
||||
|
||||
self.searchDisposable.set((combineLatest(foundItems, self.themeAndStringsPromise.get())
|
||||
|> deliverOnMainQueue).start(next: { [weak self] entries, themeAndStrings in
|
||||
if let strongSelf = self {
|
||||
let previousEntries = previousSearchItems.swap(entries)
|
||||
let firstTime = previousEntries == nil
|
||||
let transition = channelDiscussionGroupSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, account: context.account, theme: themeAndStrings.0, strings: themeAndStrings.1, interaction: interaction)
|
||||
strongSelf.enqueueTransition(transition, firstTime: firstTime)
|
||||
}
|
||||
}))
|
||||
|
||||
self.presentationDataDisposable = (context.sharedContext.presentationData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
let previousTheme = strongSelf.presentationData.theme
|
||||
let previousStrings = strongSelf.presentationData.strings
|
||||
|
||||
strongSelf.presentationData = presentationData
|
||||
|
||||
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
|
||||
strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.listNode.beganInteractiveDragging = { [weak self] in
|
||||
self?.dismissInput?()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.searchDisposable.dispose()
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.listNode.backgroundColor = theme.chatList.backgroundColor
|
||||
}
|
||||
|
||||
override func searchTextUpdated(text: String) {
|
||||
if text.isEmpty {
|
||||
self.searchQuery.set(.single(nil))
|
||||
} else {
|
||||
self.searchQuery.set(.single(text))
|
||||
}
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: ChannelDiscussionGroupSearchContainerTransition, firstTime: Bool) {
|
||||
self.enqueuedTransitions.append((transition, firstTime))
|
||||
|
||||
if self.hasValidLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
if let (transition, _) = self.enqueuedTransitions.first {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
options.insert(.PreferSynchronousDrawing)
|
||||
options.insert(.PreferSynchronousResourceLoading)
|
||||
|
||||
let isSearching = transition.isSearching
|
||||
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
self?.listNode.isHidden = !isSearching
|
||||
self?.dimNode.isHidden = isSearching
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
||||
|
||||
var duration: Double = 0.0
|
||||
var curve: UInt = 0
|
||||
switch transition {
|
||||
case .immediate:
|
||||
break
|
||||
case let .animated(animationDuration, animationCurve):
|
||||
duration = animationDuration
|
||||
switch animationCurve {
|
||||
case .easeInOut, .custom:
|
||||
break
|
||||
case .spring:
|
||||
curve = 7
|
||||
}
|
||||
}
|
||||
|
||||
let listViewCurve: ListViewAnimationCurve
|
||||
if curve == 7 {
|
||||
listViewCurve = .Spring(duration: duration)
|
||||
} else {
|
||||
listViewCurve = .Default(duration: nil)
|
||||
}
|
||||
|
||||
var insets = layout.insets(options: [.input])
|
||||
insets.top += navigationBarHeight
|
||||
insets.left += layout.safeInsets.left
|
||||
insets.right += layout.safeInsets.right
|
||||
|
||||
self.dimNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
if !hasValidLayout {
|
||||
hasValidLayout = true
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override 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 })
|
||||
}
|
||||
|
||||
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.cancel?()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user