mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00

Added ability to download music without streaming Added progress indicators for various blocking tasks Fixed image gallery swipe to dismiss after zooming Added online member count indication in supergroups Fixed contact statuses in contact search
878 lines
40 KiB
Swift
878 lines
40 KiB
Swift
import Foundation
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
|
|
private enum ContactListNodeEntryId: Hashable {
|
|
case search
|
|
case option(index: Int)
|
|
case peerId(Int64)
|
|
case deviceContact(DeviceContactStableId)
|
|
|
|
var hashValue: Int {
|
|
switch self {
|
|
case .search:
|
|
return 0
|
|
case let .option(index):
|
|
return (index + 2).hashValue
|
|
case let .peerId(peerId):
|
|
return peerId.hashValue
|
|
case let .deviceContact(id):
|
|
return id.hashValue
|
|
}
|
|
}
|
|
|
|
static func <(lhs: ContactListNodeEntryId, rhs: ContactListNodeEntryId) -> Bool {
|
|
return lhs.hashValue < rhs.hashValue
|
|
}
|
|
|
|
static func ==(lhs: ContactListNodeEntryId, rhs: ContactListNodeEntryId) -> Bool {
|
|
switch lhs {
|
|
case .search:
|
|
switch rhs {
|
|
case .search:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case let .option(index):
|
|
if case .option(index) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .peerId(lhsId):
|
|
switch rhs {
|
|
case let .peerId(rhsId):
|
|
return lhsId == rhsId
|
|
default:
|
|
return false
|
|
}
|
|
case let .deviceContact(id):
|
|
if case .deviceContact(id) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class ContactListNodeInteraction {
|
|
let activateSearch: () -> Void
|
|
let openPeer: (ContactListPeer) -> Void
|
|
|
|
init(activateSearch: @escaping () -> Void, openPeer: @escaping (ContactListPeer) -> Void) {
|
|
self.activateSearch = activateSearch
|
|
self.openPeer = openPeer
|
|
}
|
|
}
|
|
|
|
enum ContactListPeerId: Hashable {
|
|
case peer(PeerId)
|
|
case deviceContact(DeviceContactStableId)
|
|
}
|
|
|
|
enum ContactListPeer: Equatable {
|
|
case peer(peer: Peer, isGlobal: Bool)
|
|
case deviceContact(DeviceContactStableId, DeviceContactBasicData)
|
|
|
|
var id: ContactListPeerId {
|
|
switch self {
|
|
case let .peer(peer, _):
|
|
return .peer(peer.id)
|
|
case let .deviceContact(id, _):
|
|
return .deviceContact(id)
|
|
}
|
|
}
|
|
|
|
var indexName: PeerIndexNameRepresentation {
|
|
switch self {
|
|
case let .peer(peer, _):
|
|
return peer.indexName
|
|
case let .deviceContact(_, contact):
|
|
return .personName(first: contact.firstName, last: contact.lastName, addressName: "", phoneNumber: "")
|
|
}
|
|
}
|
|
|
|
static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool {
|
|
switch lhs {
|
|
case let .peer(lhsPeer, lhsIsGlobal):
|
|
if case let .peer(rhsPeer, rhsIsGlobal) = rhs, lhsPeer.isEqual(rhsPeer), lhsIsGlobal == rhsIsGlobal {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .deviceContact(id, contact):
|
|
if case .deviceContact(id, contact) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum ContactListNodeEntry: Comparable, Identifiable {
|
|
case search(PresentationTheme, PresentationStrings)
|
|
case option(Int, ContactListAdditionalOption, PresentationTheme, PresentationStrings)
|
|
case peer(Int, ContactListPeer, PeerPresence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool)
|
|
|
|
var stableId: ContactListNodeEntryId {
|
|
switch self {
|
|
case .search:
|
|
return .search
|
|
case let .option(index, _, _, _):
|
|
return .option(index: index)
|
|
case let .peer(_, peer, _, _, _, _, _, _, _, _, _):
|
|
switch peer {
|
|
case let .peer(peer, _):
|
|
return .peerId(peer.id.toInt64())
|
|
case let .deviceContact(id, _):
|
|
return .deviceContact(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func item(account: Account, interaction: ContactListNodeInteraction) -> ListViewItem {
|
|
switch self {
|
|
case let .search(theme, strings):
|
|
return ChatListSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: {
|
|
interaction.activateSearch()
|
|
})
|
|
case let .option(_, option, theme, _):
|
|
return ContactListActionItem(theme: theme, title: option.title, icon: option.icon, action: option.action)
|
|
case let .peer(_, peer, presence, header, selection, theme, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, enabled):
|
|
let status: ContactsPeerItemStatus
|
|
let itemPeer: ContactsPeerItemPeer
|
|
switch peer {
|
|
case let .peer(peer, isGlobal):
|
|
if isGlobal, let _ = peer.addressName {
|
|
status = .addressName("")
|
|
} else {
|
|
let presence = presence ?? TelegramUserPresence(status: .none)
|
|
status = .presence(presence, dateTimeFormat)
|
|
}
|
|
itemPeer = .peer(peer: peer, chatPeer: peer)
|
|
case let .deviceContact(id, contact):
|
|
status = .none
|
|
itemPeer = .deviceContact(stableId: id, contact: contact)
|
|
}
|
|
return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: account, peerMode: .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
|
|
interaction.openPeer(peer)
|
|
})
|
|
}
|
|
}
|
|
|
|
static func ==(lhs: ContactListNodeEntry, rhs: ContactListNodeEntry) -> Bool {
|
|
switch lhs {
|
|
case let .search(lhsTheme, lhsStrings):
|
|
if case let .search(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .option(lhsIndex, lhsOption, lhsTheme, lhsStrings):
|
|
if case let .option(rhsIndex, rhsOption, rhsTheme, rhsStrings) = rhs, lhsIndex == rhsIndex, lhsOption == rhsOption, lhsTheme === rhsTheme, lhsStrings === rhsStrings {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsEnabled):
|
|
switch rhs {
|
|
case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsEnabled):
|
|
if lhsIndex != rhsIndex {
|
|
return false
|
|
}
|
|
if lhsPeer != rhsPeer {
|
|
return false
|
|
}
|
|
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
|
|
if !lhsPresence.isEqual(to: rhsPresence) {
|
|
return false
|
|
}
|
|
} else if (lhsPresence != nil) != (rhsPresence != nil) {
|
|
return false
|
|
}
|
|
if lhsHeader?.id != rhsHeader?.id {
|
|
return false
|
|
}
|
|
if lhsSelection != rhsSelection {
|
|
return false
|
|
}
|
|
if lhsTheme !== rhsTheme {
|
|
return false
|
|
}
|
|
if lhsStrings !== rhsStrings {
|
|
return false
|
|
}
|
|
if lhsTimeFormat != rhsTimeFormat {
|
|
return false
|
|
}
|
|
if lhsSortOrder != rhsSortOrder {
|
|
return false
|
|
}
|
|
if lhsDisplayOrder != rhsDisplayOrder {
|
|
return false
|
|
}
|
|
if lhsEnabled != rhsEnabled {
|
|
return false
|
|
}
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
static func <(lhs: ContactListNodeEntry, rhs: ContactListNodeEntry) -> Bool {
|
|
switch lhs {
|
|
case .search:
|
|
return true
|
|
case let .option(lhsIndex, _, _, _):
|
|
switch rhs {
|
|
case .search:
|
|
return false
|
|
case let .option(rhsIndex, _, _, _):
|
|
return lhsIndex < rhsIndex
|
|
case .peer:
|
|
return true
|
|
}
|
|
case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _):
|
|
switch rhs {
|
|
case .search, .option:
|
|
return false
|
|
case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _):
|
|
return lhsIndex < rhsIndex
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension PeerIndexNameRepresentation {
|
|
func isLessThan(other: PeerIndexNameRepresentation, ordering: PresentationPersonNameOrder) -> ComparisonResult {
|
|
switch self {
|
|
case let .title(lhsTitle, _):
|
|
switch other {
|
|
case let .title(title, _):
|
|
return lhsTitle.compare(title)
|
|
case let .personName(_, last, _, _):
|
|
let lastResult = lhsTitle.compare(last)
|
|
if lastResult == .orderedSame {
|
|
return .orderedAscending
|
|
} else {
|
|
return lastResult
|
|
}
|
|
}
|
|
case let .personName(lhsFirst, lhsLast, _, _):
|
|
switch other {
|
|
case let .title(title, _):
|
|
let lastResult = lhsFirst.compare(title)
|
|
if lastResult == .orderedSame {
|
|
return .orderedDescending
|
|
} else {
|
|
return lastResult
|
|
}
|
|
case let .personName(first, last, _, _):
|
|
switch ordering {
|
|
case .firstLast:
|
|
let firstResult = lhsFirst.compare(first)
|
|
if firstResult == .orderedSame {
|
|
return lhsLast.compare(last)
|
|
} else {
|
|
return firstResult
|
|
}
|
|
case .lastFirst:
|
|
let lastResult = lhsLast.compare(last)
|
|
if lastResult == .orderedSame {
|
|
return lhsFirst.compare(first)
|
|
} else {
|
|
return lastResult
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer], presences: [PeerId: PeerPresence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds:Set<PeerId>) -> [ContactListNodeEntry] {
|
|
var entries: [ContactListNodeEntry] = []
|
|
|
|
var orderedPeers: [ContactListPeer]
|
|
var headers: [ContactListPeerId: ContactListNameIndexHeader] = [:]
|
|
|
|
switch presentation {
|
|
case let .orderedByPresence(options):
|
|
entries.append(.search(theme, strings))
|
|
orderedPeers = peers.sorted(by: { lhs, rhs in
|
|
if case let .peer(lhsPeer, _) = lhs, case let .peer(rhsPeer, _) = rhs {
|
|
let lhsPresence = presences[lhsPeer.id]
|
|
let rhsPresence = presences[rhsPeer.id]
|
|
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
|
|
} else if case .peer = lhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
})
|
|
for i in 0 ..< options.count {
|
|
entries.append(.option(i, options[i], theme, strings))
|
|
}
|
|
case let .natural(displaySearch, options):
|
|
orderedPeers = peers.sorted(by: { lhs, rhs in
|
|
let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: sortOrder)
|
|
if result == .orderedSame {
|
|
if case let .peer(lhsPeer, _) = lhs, case let .peer(rhsPeer, _) = rhs {
|
|
return lhsPeer.id < rhsPeer.id
|
|
} else if case let .deviceContact(lhsId, _) = lhs, case let .deviceContact(rhsId, _) = rhs {
|
|
return lhsId < rhsId
|
|
} else if case .peer = lhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
} else {
|
|
return result == .orderedAscending
|
|
}
|
|
})
|
|
var headerCache: [unichar: ContactListNameIndexHeader] = [:]
|
|
for peer in orderedPeers {
|
|
var indexHeader: unichar = 35
|
|
switch peer.indexName {
|
|
case let .title(title, _):
|
|
if let c = title.utf16.first {
|
|
indexHeader = c
|
|
}
|
|
case let .personName(first, last, _, _):
|
|
switch sortOrder {
|
|
case .firstLast:
|
|
if let c = first.utf16.first {
|
|
indexHeader = c
|
|
} else if let c = last.utf16.first {
|
|
indexHeader = c
|
|
}
|
|
case .lastFirst:
|
|
if let c = last.utf16.first {
|
|
indexHeader = c
|
|
} else if let c = first.utf16.first {
|
|
indexHeader = c
|
|
}
|
|
}
|
|
}
|
|
let header: ContactListNameIndexHeader
|
|
if let cached = headerCache[indexHeader] {
|
|
header = cached
|
|
} else {
|
|
header = ContactListNameIndexHeader(theme: theme, letter: indexHeader)
|
|
headerCache[indexHeader] = header
|
|
}
|
|
headers[peer.id] = header
|
|
}
|
|
if displaySearch {
|
|
entries.append(.search(theme, strings))
|
|
}
|
|
for i in 0 ..< options.count {
|
|
entries.append(.option(i, options[i], theme, strings))
|
|
}
|
|
case .search:
|
|
orderedPeers = peers
|
|
}
|
|
|
|
var removeIndices: [Int] = []
|
|
for i in 0 ..< orderedPeers.count {
|
|
switch orderedPeers[i].indexName {
|
|
case let .title(title, _):
|
|
if title.isEmpty {
|
|
removeIndices.append(i)
|
|
}
|
|
case let .personName(first, last, _, _):
|
|
if first.isEmpty && last.isEmpty {
|
|
removeIndices.append(i)
|
|
}
|
|
}
|
|
}
|
|
if !removeIndices.isEmpty {
|
|
for index in removeIndices.reversed() {
|
|
orderedPeers.remove(at: index)
|
|
}
|
|
}
|
|
|
|
var commonHeader: ListViewItemHeader?
|
|
switch presentation {
|
|
case .orderedByPresence:
|
|
commonHeader = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil)
|
|
default:
|
|
break
|
|
}
|
|
|
|
for i in 0 ..< orderedPeers.count {
|
|
let selection: ContactsPeerItemSelection
|
|
if let selectionState = selectionState {
|
|
selection = .selectable(selected: selectionState.selectedPeerIndices[orderedPeers[i].id] != nil)
|
|
} else {
|
|
selection = .none
|
|
}
|
|
let header: ListViewItemHeader?
|
|
switch presentation {
|
|
case .orderedByPresence:
|
|
header = commonHeader
|
|
default:
|
|
header = headers[orderedPeers[i].id]
|
|
}
|
|
var presence: PeerPresence?
|
|
if case let .peer(peer, _) = orderedPeers[i] {
|
|
presence = presences[peer.id]
|
|
}
|
|
let enabled: Bool
|
|
switch orderedPeers[i] {
|
|
case let .peer(peer, _):
|
|
enabled = !disabledPeerIds.contains(peer.id)
|
|
default:
|
|
enabled = true
|
|
}
|
|
entries.append(.peer(i, orderedPeers[i], presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, enabled))
|
|
}
|
|
return entries
|
|
}
|
|
|
|
private func preparedContactListNodeTransition(account: Account, from fromEntries: [ContactListNodeEntry], to toEntries: [ContactListNodeEntry], interaction: ContactListNodeInteraction, firstTime: Bool, animated: Bool) -> ContactsListNodeTransition {
|
|
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, interaction: interaction), directionHint: nil) }
|
|
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction), directionHint: nil) }
|
|
|
|
return ContactsListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, animated: animated)
|
|
}
|
|
|
|
private struct ContactsListNodeTransition {
|
|
let deletions: [ListViewDeleteItem]
|
|
let insertions: [ListViewInsertItem]
|
|
let updates: [ListViewUpdateItem]
|
|
let firstTime: Bool
|
|
let animated: Bool
|
|
}
|
|
|
|
public struct ContactListAdditionalOption: Equatable {
|
|
public let title: String
|
|
public let icon: UIImage?
|
|
public let action: () -> Void
|
|
|
|
public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool {
|
|
return lhs.title == rhs.title && lhs.icon === rhs.icon
|
|
}
|
|
}
|
|
|
|
enum ContactListPresentation {
|
|
case orderedByPresence(options: [ContactListAdditionalOption])
|
|
case natural(displaySearch: Bool, options: [ContactListAdditionalOption])
|
|
case search(signal: Signal<String, NoError>, searchDeviceContacts: Bool)
|
|
}
|
|
|
|
struct ContactListNodeGroupSelectionState: Equatable {
|
|
let selectedPeerIndices: [ContactListPeerId: Int]
|
|
let nextSelectionIndex: Int
|
|
|
|
private init(selectedPeerIndices: [ContactListPeerId: Int], nextSelectionIndex: Int) {
|
|
self.selectedPeerIndices = selectedPeerIndices
|
|
self.nextSelectionIndex = nextSelectionIndex
|
|
}
|
|
|
|
init() {
|
|
self.selectedPeerIndices = [:]
|
|
self.nextSelectionIndex = 0
|
|
}
|
|
|
|
func withToggledPeerId(_ peerId: ContactListPeerId) -> ContactListNodeGroupSelectionState {
|
|
var updatedIndices = self.selectedPeerIndices
|
|
if let _ = updatedIndices[peerId] {
|
|
updatedIndices.removeValue(forKey: peerId)
|
|
return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, nextSelectionIndex: self.nextSelectionIndex)
|
|
} else {
|
|
updatedIndices[peerId] = self.nextSelectionIndex
|
|
return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, nextSelectionIndex: self.nextSelectionIndex + 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ContactListFilter {
|
|
case excludeSelf
|
|
case exclude([PeerId])
|
|
case disable([PeerId])
|
|
}
|
|
|
|
final class ContactListNode: ASDisplayNode {
|
|
private let account: Account
|
|
private let presentation: ContactListPresentation
|
|
private let filters: [ContactListFilter]
|
|
|
|
let listNode: ListView
|
|
|
|
private var queuedTransitions: [ContactsListNodeTransition] = []
|
|
private var hasValidLayout = false
|
|
|
|
private var _ready = ValuePromise<Bool>()
|
|
var ready: Signal<Bool, NoError> {
|
|
return self._ready.get()
|
|
}
|
|
private var didSetReady = false
|
|
|
|
private let contactPeersViewPromise = Promise<ContactPeersView>()
|
|
|
|
private let selectionStatePromise = Promise<ContactListNodeGroupSelectionState?>(nil)
|
|
private var selectionStateValue: ContactListNodeGroupSelectionState? {
|
|
didSet {
|
|
self.selectionStatePromise.set(.single(self.selectionStateValue))
|
|
}
|
|
}
|
|
|
|
private var enableUpdatesValue = false
|
|
var enableUpdates: Bool {
|
|
get {
|
|
return self.enableUpdatesValue
|
|
} set(value) {
|
|
if value != self.enableUpdatesValue {
|
|
self.enableUpdatesValue = value
|
|
if value {
|
|
self.contactPeersViewPromise.set(self.account.postbox.contactPeersView(accountPeerId: self.account.peerId, includePresences: true) |> mapToThrottled { next -> Signal<ContactPeersView, NoError> in
|
|
return .single(next) |> then(.complete() |> delay(5.0, queue: Queue.concurrentDefaultQueue()))
|
|
})
|
|
} else {
|
|
self.contactPeersViewPromise.set(self.account.postbox.contactPeersView(accountPeerId: self.account.peerId, includePresences: true) |> take(1))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var activateSearch: (() -> Void)?
|
|
var openPeer: ((ContactListPeer) -> Void)?
|
|
|
|
private let previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil)
|
|
private let disposable = MetaDisposable()
|
|
|
|
private var presentationData: PresentationData
|
|
private var presentationDataDisposable: Disposable?
|
|
private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool)>
|
|
|
|
init(account: Account, presentation: ContactListPresentation, filters: [ContactListFilter] = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil) {
|
|
self.account = account
|
|
self.presentation = presentation
|
|
self.filters = filters
|
|
|
|
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
|
|
|
self.listNode = ListView()
|
|
self.listNode.dynamicBounceEnabled = !self.presentationData.disableAnimations
|
|
|
|
self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.dateTimeFormat, self.presentationData.nameSortOrder, self.presentationData.nameDisplayOrder, self.presentationData.disableAnimations))
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
|
|
|
|
self.selectionStateValue = selectionState
|
|
self.selectionStatePromise.set(.single(selectionState))
|
|
|
|
self.addSubnode(self.listNode)
|
|
|
|
let processingQueue = Queue()
|
|
let previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil)
|
|
|
|
let interaction = ContactListNodeInteraction(activateSearch: { [weak self] in
|
|
self?.activateSearch?()
|
|
}, openPeer: { [weak self] peer in
|
|
self?.openPeer?(peer)
|
|
})
|
|
|
|
let account = self.account
|
|
var firstTime: Int32 = 1
|
|
let selectionStateSignal = self.selectionStatePromise.get()
|
|
let transition: Signal<ContactsListNodeTransition, NoError>
|
|
let themeAndStringsPromise = self.themeAndStringsPromise
|
|
if case let .search(query, searchDeviceContacts) = presentation {
|
|
transition = query
|
|
|> mapToSignal { query in
|
|
let foundLocalContacts = account.postbox.searchContacts(query: query.lowercased())
|
|
let foundRemoteContacts: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], []))
|
|
|> then(
|
|
searchPeers(account: account, query: query)
|
|
|> map { ($0.0, $0.1) }
|
|
|> delay(0.2, queue: Queue.concurrentDefaultQueue())
|
|
)
|
|
let foundDeviceContacts: Signal<[DeviceContactStableId: DeviceContactBasicData], NoError>
|
|
if searchDeviceContacts {
|
|
foundDeviceContacts = account.telegramApplicationContext.contactDataManager.search(query: query)
|
|
} else {
|
|
foundDeviceContacts = .single([:])
|
|
}
|
|
|
|
return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, selectionStateSignal, themeAndStringsPromise.get())
|
|
|> mapToQueue { localPeersAndStatuses, remotePeers, deviceContacts, selectionState, themeAndStrings -> Signal<ContactsListNodeTransition, NoError> in
|
|
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
|
|
var existingPeerIds = Set<PeerId>()
|
|
var disabledPeerIds = Set<PeerId>()
|
|
|
|
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
|
|
for filter in filters {
|
|
switch filter {
|
|
case .excludeSelf:
|
|
existingPeerIds.insert(account.peerId)
|
|
case let .exclude(peerIds):
|
|
existingPeerIds = existingPeerIds.union(peerIds)
|
|
case let .disable(peerIds):
|
|
disabledPeerIds = disabledPeerIds.union(peerIds)
|
|
}
|
|
}
|
|
|
|
var peers: [ContactListPeer] = []
|
|
for peer in localPeersAndStatuses.0 {
|
|
if !existingPeerIds.contains(peer.id) {
|
|
existingPeerIds.insert(peer.id)
|
|
peers.append(.peer(peer: peer, isGlobal: false))
|
|
if searchDeviceContacts, let user = peer as? TelegramUser, let phone = user.phone {
|
|
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
|
}
|
|
}
|
|
}
|
|
for peer in remotePeers.0 {
|
|
if peer.peer is TelegramUser {
|
|
if !existingPeerIds.contains(peer.peer.id) {
|
|
existingPeerIds.insert(peer.peer.id)
|
|
peers.append(.peer(peer: peer.peer, isGlobal: true))
|
|
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
|
|
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for peer in remotePeers.1 {
|
|
if peer.peer is TelegramUser {
|
|
if !existingPeerIds.contains(peer.peer.id) {
|
|
existingPeerIds.insert(peer.peer.id)
|
|
peers.append(.peer(peer: peer.peer, isGlobal: true))
|
|
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
|
|
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
outer: for (stableId, contact) in deviceContacts {
|
|
inner: for phoneNumber in contact.phoneNumbers {
|
|
let normalizedNumber = DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phoneNumber.value))
|
|
if existingNormalizedPhoneNumbers.contains(normalizedNumber) {
|
|
continue outer
|
|
}
|
|
}
|
|
peers.append(.deviceContact(stableId, contact))
|
|
}
|
|
|
|
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds)
|
|
let previous = previousEntries.swap(entries)
|
|
return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: false))
|
|
}
|
|
|
|
if OSAtomicCompareAndSwap32(1, 0, &firstTime) {
|
|
return signal |> runOn(Queue.mainQueue())
|
|
} else {
|
|
return signal |> runOn(processingQueue)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
transition = (combineLatest(self.contactPeersViewPromise.get(), selectionStateSignal, themeAndStringsPromise.get())
|
|
|> mapToQueue { view, selectionState, themeAndStrings -> Signal<ContactsListNodeTransition, NoError> in
|
|
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
|
|
|
|
var peers = view.peers.map({ ContactListPeer.peer(peer: $0, isGlobal: false) })
|
|
var existingPeerIds = Set<PeerId>()
|
|
var disabledPeerIds = Set<PeerId>()
|
|
for filter in filters {
|
|
switch filter {
|
|
case .excludeSelf:
|
|
existingPeerIds.insert(account.peerId)
|
|
case let .exclude(peerIds):
|
|
existingPeerIds = existingPeerIds.union(peerIds)
|
|
case let .disable(peerIds):
|
|
disabledPeerIds = disabledPeerIds.union(peerIds)
|
|
}
|
|
}
|
|
|
|
peers = peers.filter { contact in
|
|
switch contact {
|
|
case let .peer(peer, _):
|
|
return !existingPeerIds.contains(peer.id)
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: peers, presences: view.peerPresences, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds)
|
|
let previous = previousEntries.swap(entries)
|
|
let animated: Bool
|
|
if let previous = previous, !themeAndStrings.5 {
|
|
animated = (entries.count - previous.count) < 20
|
|
} else {
|
|
animated = false
|
|
}
|
|
return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: animated))
|
|
}
|
|
|
|
if OSAtomicCompareAndSwap32(1, 0, &firstTime) {
|
|
return signal |> runOn(Queue.mainQueue())
|
|
} else {
|
|
return signal |> runOn(processingQueue)
|
|
}
|
|
})
|
|
|> deliverOnMainQueue
|
|
}
|
|
self.disposable.set(transition.start(next: { [weak self] transition in
|
|
self?.enqueueTransition(transition)
|
|
}))
|
|
|
|
self.presentationDataDisposable = (account.telegramApplicationContext.presentationData
|
|
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
|
if let strongSelf = self {
|
|
let previousTheme = strongSelf.presentationData.theme
|
|
let previousStrings = strongSelf.presentationData.strings
|
|
let previousDisableAnimations = strongSelf.presentationData.disableAnimations
|
|
|
|
strongSelf.presentationData = presentationData
|
|
|
|
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings || previousDisableAnimations != presentationData.disableAnimations {
|
|
strongSelf.backgroundColor = presentationData.theme.chatList.backgroundColor
|
|
strongSelf.themeAndStringsPromise.set(.single((presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameSortOrder, presentationData.nameDisplayOrder, presentationData.disableAnimations)))
|
|
|
|
strongSelf.listNode.dynamicBounceEnabled = !presentationData.disableAnimations
|
|
|
|
strongSelf.listNode.forEachAccessoryItemNode({ accessoryItemNode in
|
|
if let accessoryItemNode = accessoryItemNode as? ContactsSectionHeaderAccessoryItemNode {
|
|
accessoryItemNode.updateTheme(theme: presentationData.theme)
|
|
}
|
|
})
|
|
|
|
strongSelf.listNode.forEachItemHeaderNode({ itemHeaderNode in
|
|
if let itemHeaderNode = itemHeaderNode as? ContactListNameIndexHeaderNode {
|
|
itemHeaderNode.updateTheme(theme: presentationData.theme)
|
|
} else if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode {
|
|
itemHeaderNode.updateTheme(theme: presentationData.theme)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
self.listNode.didEndScrolling = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
fixSearchableListNodeScrolling(strongSelf.listNode)
|
|
}
|
|
|
|
self.enableUpdates = true
|
|
}
|
|
|
|
deinit {
|
|
self.disposable.dispose()
|
|
self.presentationDataDisposable?.dispose()
|
|
}
|
|
|
|
func updateSelectionState(_ f: (ContactListNodeGroupSelectionState?) -> ContactListNodeGroupSelectionState?) {
|
|
let updatedSelectionState = f(self.selectionStateValue)
|
|
if updatedSelectionState != self.selectionStateValue {
|
|
self.selectionStateValue = updatedSelectionState
|
|
}
|
|
}
|
|
|
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
var insets = layout.insets(options: [.input])
|
|
insets.left += layout.safeInsets.left
|
|
insets.right += layout.safeInsets.right
|
|
|
|
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
|
self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
|
|
|
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:
|
|
break
|
|
case .spring:
|
|
curve = 7
|
|
}
|
|
}
|
|
|
|
let listViewCurve: ListViewAnimationCurve
|
|
if curve == 7 {
|
|
listViewCurve = .Spring(duration: duration)
|
|
} else {
|
|
listViewCurve = .Default
|
|
}
|
|
|
|
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve)
|
|
|
|
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
|
|
if !self.hasValidLayout {
|
|
self.hasValidLayout = true
|
|
self.dequeueTransitions()
|
|
}
|
|
}
|
|
|
|
private func enqueueTransition(_ transition: ContactsListNodeTransition) {
|
|
self.queuedTransitions.append(transition)
|
|
|
|
if self.hasValidLayout {
|
|
self.dequeueTransitions()
|
|
}
|
|
}
|
|
|
|
private func dequeueTransitions() {
|
|
if self.hasValidLayout {
|
|
while !self.queuedTransitions.isEmpty {
|
|
let transition = self.queuedTransitions.removeFirst()
|
|
|
|
var options = ListViewDeleteAndInsertOptions()
|
|
if transition.firstTime {
|
|
options.insert(.Synchronous)
|
|
options.insert(.LowLatency)
|
|
} else if transition.animated {
|
|
if case .orderedByPresence = self.presentation {
|
|
options.insert(.AnimateCrossfade)
|
|
}
|
|
}
|
|
self.listNode.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(true)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func scrollToTop() {
|
|
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
}
|
|
}
|