mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
336 lines
12 KiB
Swift
336 lines
12 KiB
Swift
import Foundation
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
import TelegramCore
|
|
|
|
private enum ContactsControllerEntryId: Hashable {
|
|
case search
|
|
case vcard
|
|
case peerId(Int64)
|
|
|
|
var hashValue: Int {
|
|
switch self {
|
|
case .search:
|
|
return 0
|
|
case .vcard:
|
|
return 1
|
|
case let .peerId(peerId):
|
|
return peerId.hashValue
|
|
}
|
|
}
|
|
}
|
|
|
|
private func <(lhs: ContactsControllerEntryId, rhs: ContactsControllerEntryId) -> Bool {
|
|
return lhs.hashValue < rhs.hashValue
|
|
}
|
|
|
|
private func ==(lhs: ContactsControllerEntryId, rhs: ContactsControllerEntryId) -> Bool {
|
|
switch lhs {
|
|
case .search:
|
|
switch rhs {
|
|
case .search:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case .vcard:
|
|
switch rhs {
|
|
case .vcard:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case let .peerId(lhsId):
|
|
switch rhs {
|
|
case let .peerId(rhsId):
|
|
return lhsId == rhsId
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum ContactsEntry: Comparable, Identifiable {
|
|
case search
|
|
case vcard(Peer)
|
|
case peer(Peer, PeerPresence?)
|
|
|
|
var stableId: ContactsControllerEntryId {
|
|
switch self {
|
|
case .search:
|
|
return .search
|
|
case .vcard:
|
|
return .vcard
|
|
case let .peer(peer, _):
|
|
return .peerId(peer.id.toInt64())
|
|
}
|
|
}
|
|
|
|
func item(account: Account, index: PeerNameIndex, interaction: ContactsControllerInteraction) -> ListViewItem {
|
|
switch self {
|
|
case .search:
|
|
return ChatListSearchItem(placeholder: "Search contacts", activate: {
|
|
interaction.activateSearch()
|
|
})
|
|
case let .vcard(peer):
|
|
return ContactsVCardItem(account: account, peer: peer, action: { peer in
|
|
interaction.openPeer(peer.id)
|
|
})
|
|
case let .peer(peer, presence):
|
|
return ContactsPeerItem(account: account, peer: peer, presence: presence, index: nil, action: { _ in
|
|
interaction.openPeer(peer.id)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private func ==(lhs: ContactsEntry, rhs: ContactsEntry) -> Bool {
|
|
switch lhs {
|
|
case .search:
|
|
switch rhs {
|
|
case .search:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case let .vcard(lhsPeer):
|
|
switch rhs {
|
|
case let .vcard(rhsPeer):
|
|
return lhsPeer.id == rhsPeer.id
|
|
default:
|
|
return false
|
|
}
|
|
case let .peer(lhsPeer, lhsPresence):
|
|
switch rhs {
|
|
case let .peer(rhsPeer, rhsPresence):
|
|
if lhsPeer.id != rhsPeer.id {
|
|
return false
|
|
}
|
|
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
|
|
if !lhsPresence.isEqual(to: rhsPresence) {
|
|
return false
|
|
}
|
|
} else if (lhsPresence != nil) != (rhsPresence != nil) {
|
|
return false
|
|
}
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private func <(lhs: ContactsEntry, rhs: ContactsEntry) -> Bool {
|
|
switch lhs {
|
|
case .search:
|
|
return true
|
|
case .vcard:
|
|
switch rhs {
|
|
case .search, .vcard:
|
|
return false
|
|
case .peer:
|
|
return true
|
|
}
|
|
case let .peer(lhsPeer, lhsPresence):
|
|
switch rhs {
|
|
case .search:
|
|
return false
|
|
case .vcard:
|
|
return false
|
|
case let .peer(rhsPeer, rhsPresence):
|
|
if let lhsPresence = lhsPresence as? TelegramUserPresence, let rhsPresence = rhsPresence as? TelegramUserPresence {
|
|
if lhsPresence.status < rhsPresence.status {
|
|
return false
|
|
} else if lhsPresence.status > rhsPresence.status {
|
|
return true
|
|
}
|
|
} else if let _ = lhsPresence {
|
|
return true
|
|
} else if let _ = rhsPresence {
|
|
return false
|
|
}
|
|
return lhsPeer.id < rhsPeer.id
|
|
}
|
|
}
|
|
}
|
|
|
|
private func contactListEntries(_ view: ContactPeersView) -> [ContactsEntry] {
|
|
var entries: [ContactsEntry] = []
|
|
entries.append(.search)
|
|
if let peer = view.accountPeer {
|
|
entries.append(.vcard(peer))
|
|
}
|
|
for peer in view.peers {
|
|
entries.append(.peer(peer, view.peerPresences[peer.id]))
|
|
}
|
|
entries.sort()
|
|
return entries
|
|
}
|
|
|
|
private struct ContactsListTransition {
|
|
let deletions: [ListViewDeleteItem]
|
|
let insertions: [ListViewInsertItem]
|
|
let updates: [ListViewUpdateItem]
|
|
}
|
|
|
|
private final class ContactsControllerInteraction {
|
|
let openPeer: (PeerId) -> Void
|
|
let activateSearch: () -> Void
|
|
|
|
init(openPeer: @escaping (PeerId) -> Void, activateSearch: @escaping () -> Void) {
|
|
self.openPeer = openPeer
|
|
self.activateSearch = activateSearch
|
|
}
|
|
}
|
|
|
|
private func preparedContactsListTransition(account: Account, index: PeerNameIndex, from fromEntries: [ContactsEntry], to toEntries: [ContactsEntry], interaction: ContactsControllerInteraction) -> ContactsListTransition {
|
|
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, index: index, interaction: interaction), directionHint: nil) }
|
|
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, index: index, interaction: interaction), directionHint: nil) }
|
|
|
|
return ContactsListTransition(deletions: deletions, insertions: insertions, updates: updates)
|
|
}
|
|
|
|
public class ContactsController: ViewController {
|
|
private let queue = Queue()
|
|
|
|
private let account: Account
|
|
private let transitionDisposable = MetaDisposable()
|
|
|
|
private var contactsNode: ContactsControllerNode {
|
|
return self.displayNode as! ContactsControllerNode
|
|
}
|
|
|
|
private let index: PeerNameIndex = .lastNameFirst
|
|
|
|
private var _ready = Promise<Bool>()
|
|
override public var ready: Promise<Bool> {
|
|
return self._ready
|
|
}
|
|
private var didSetReady = false
|
|
|
|
private let previousEntries = Atomic<[ContactsEntry]?>(value: nil)
|
|
|
|
public init(account: Account) {
|
|
self.account = account
|
|
|
|
super.init()
|
|
|
|
self.title = "Contacts"
|
|
self.tabBarItem.title = "Contacts"
|
|
self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconContacts")
|
|
self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconContactsSelected")
|
|
|
|
self.scrollToTop = { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.contactsNode.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
}
|
|
}
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.transitionDisposable.dispose()
|
|
}
|
|
|
|
override public func loadDisplayNode() {
|
|
self.displayNode = ContactsControllerNode(account: self.account)
|
|
|
|
self.contactsNode.navigationBar = self.navigationBar
|
|
|
|
self.contactsNode.requestDeactivateSearch = { [weak self] in
|
|
self?.deactivateSearch()
|
|
}
|
|
|
|
self.contactsNode.requestOpenPeerFromSearch = { [weak self] peerId in
|
|
if let strongSelf = self {
|
|
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId))
|
|
}
|
|
}
|
|
|
|
self.displayNodeDidLoad()
|
|
}
|
|
|
|
override public func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
let interaction = ContactsControllerInteraction(openPeer: { [weak self] peerId in
|
|
if let strongSelf = self {
|
|
strongSelf.contactsNode.listView.clearHighlightAnimated(true)
|
|
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId))
|
|
}
|
|
}, activateSearch: { [weak self] in
|
|
self?.activateSearch()
|
|
})
|
|
|
|
let account = self.account
|
|
let index = self.index
|
|
let previousEntries = self.previousEntries
|
|
let transition = account.postbox.contactPeersView(index: self.index, accountPeerId: account.peerId)
|
|
|> map { view -> (ContactsListTransition, Bool, Bool) in
|
|
let entries = contactListEntries(view)
|
|
let previous = previousEntries.swap(entries)
|
|
return (preparedContactsListTransition(account: account, index: index, from: previous ?? [], to: entries, interaction: interaction), previous == nil, previous != nil)
|
|
}
|
|
|> deliverOnMainQueue
|
|
|
|
self.transitionDisposable.set(transition.start(next: { [weak self] (transition, firstTime, animated) in
|
|
self?.enqueueTransition(transition, firstTime: firstTime, animated: animated)
|
|
}))
|
|
}
|
|
|
|
override public func viewDidDisappear(_ animated: Bool) {
|
|
super.viewDidDisappear(animated)
|
|
|
|
self.transitionDisposable.set(nil)
|
|
}
|
|
|
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
super.containerLayoutUpdated(layout, transition: transition)
|
|
|
|
self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition)
|
|
}
|
|
|
|
private func enqueueTransition(_ transition: ContactsListTransition, firstTime: Bool, animated: Bool) {
|
|
var options = ListViewDeleteAndInsertOptions()
|
|
if firstTime {
|
|
options.insert(.Synchronous)
|
|
options.insert(.LowLatency)
|
|
} else if animated {
|
|
options.insert(.AnimateInsertion)
|
|
}
|
|
self.contactsNode.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
if !strongSelf.didSetReady {
|
|
strongSelf.didSetReady = true
|
|
strongSelf._ready.set(.single(true))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
private func activateSearch() {
|
|
if self.displayNavigationBar {
|
|
if let scrollToTop = self.scrollToTop {
|
|
scrollToTop()
|
|
}
|
|
self.contactsNode.activateSearch()
|
|
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
|
|
}
|
|
}
|
|
|
|
private func deactivateSearch() {
|
|
if !self.displayNavigationBar {
|
|
self.contactsNode.deactivateSearch()
|
|
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
|
|
}
|
|
}
|
|
}
|