import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore import Postbox import TelegramPresentationData import TelegramUIPreferences import DeviceAccess import MergeLists import ItemListUI import PresentationDataUtils import MediaResources import AccountContext import TelegramPermissions import TelegramNotices import ContactsPeerItem import ChatListSearchItemNode import ChatListSearchItemHeader import SearchUI import TelegramPermissionsUI import AppBundle import ContextUI import PhoneNumberFormat import LocalizedPeerData import ContextUI private let dropDownIcon = { () -> UIImage in UIGraphicsBeginImageContextWithOptions(CGSize(width: 12.0, height: 12.0), false, 0.0) let context = UIGraphicsGetCurrentContext()! context.setBlendMode(.copy) context.setFillColor(UIColor.black.cgColor) context.move(to: CGPoint(x: 0.0, y: 3.0)) context.addLine(to: CGPoint(x: 12.0, y: 3.0)) context.addLine(to: CGPoint(x: 6.0, y: 9.0)) context.fillPath() let image = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return image }() private enum ContactListNodeEntrySection: Int { case stories = 0 case contacts = 1 } private enum ContactListNodeEntryId: Hashable { case search case sort case permission(action: Bool) case option(index: Int) case peerId(peerId: Int64, section: ContactListNodeEntrySection) case deviceContact(DeviceContactStableId) } private final class ContactListNodeInteraction { fileprivate let activateSearch: () -> Void fileprivate let authorize: () -> Void fileprivate let suppressWarning: () -> Void fileprivate let openPeer: (ContactListPeer, ContactListAction, ASDisplayNode?, ContextGesture?) -> Void fileprivate let openDisabledPeer: (EnginePeer, ChatListDisabledPeerReason) -> Void fileprivate let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)? fileprivate let openStories: (EnginePeer, ASDisplayNode) -> Void fileprivate let deselectAll: () -> Void fileprivate let toggleSelection: ([EnginePeer], Bool) -> Void let itemHighlighting = ContactItemHighlighting() init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction, ASDisplayNode?, ContextGesture?) -> Void, openDisabledPeer: @escaping (EnginePeer, ChatListDisabledPeerReason) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?, openStories: @escaping (EnginePeer, ASDisplayNode) -> Void, deselectAll: @escaping () -> Void, toggleSelection: @escaping ([EnginePeer], Bool) -> Void) { self.activateSearch = activateSearch self.authorize = authorize self.suppressWarning = suppressWarning self.openPeer = openPeer self.openDisabledPeer = openDisabledPeer self.contextAction = contextAction self.openStories = openStories self.deselectAll = deselectAll self.toggleSelection = toggleSelection } } enum ContactListAnimation { case none case `default` case insertion } private enum ContactListNodeEntry: Comparable, Identifiable { struct StoryData: Equatable { var count: Int var unseenCount: Int var hasUnseenCloseFriends: Bool } case search(PresentationTheme, PresentationStrings) case sort(PresentationTheme, PresentationStrings, ContactsSortOrder) case permissionInfo(PresentationTheme, String, String, Bool) case permissionEnable(PresentationTheme, String) case option(Int, ContactListAdditionalOption, ListViewItemHeader?, PresentationTheme, PresentationStrings) case peer(Int, ContactListPeer, EnginePeer.Presence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool, Bool, Bool, StoryData?, Bool) var stableId: ContactListNodeEntryId { switch self { case .search: return .search case .sort: return .sort case .permissionInfo: return .permission(action: false) case .permissionEnable: return .permission(action: true) case let .option(index, _, _, _, _): return .option(index: index) case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _, _, storyData, _): switch peer { case let .peer(peer, _, _): return .peerId(peerId: peer.id.toInt64(), section: storyData != nil ? .stories : .contacts) case let .deviceContact(id, _): return .deviceContact(id) } } } func item(context: AccountContext, presentationData: PresentationData, interaction: ContactListNodeInteraction, isSearch: Bool) -> ListViewItem { switch self { case let .search(theme, strings): return ChatListSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: { interaction.activateSearch() }) case let .sort(_, strings, sortOrder): var text = strings.Contacts_SortedByName if case .presence = sortOrder { text = strings.Contacts_SortedByPresence } return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, accessible: false, header: nil, action: { }) case let .permissionInfo(_, title, text, suppressed): return InfoListItem(presentationData: ItemListPresentationData(presentationData), title: title, text: .plain(text), style: .plain, closeAction: suppressed ? nil : { interaction.suppressWarning() }) case let .permissionEnable(_, text): return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .none, header: nil, action: { interaction.authorize() }) case let .option(_, option, header, _, _): return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, subtitle: option.subtitle, icon: option.icon, clearHighlightAutomatically: option.clearHighlightAutomatically, header: header, action: option.action) case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, hasMoreButton, enabled, storyData, requiresPremiumForMessaging): var status: ContactsPeerItemStatus let itemPeer: ContactsPeerItemPeer var isContextActionEnabled = false switch peer { case let .peer(peer, isGlobal, participantCount): isContextActionEnabled = true if isGlobal, let _ = peer.addressName { status = .addressName("") } else { if let _ = peer as? TelegramUser { status = .presence(presence ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0), dateTimeFormat) } else if let group = peer as? TelegramGroup { status = .custom(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), multiline: false, isActive: false, icon: nil) } else if let channel = peer as? TelegramChannel { if case .group = channel.info { if let participantCount = participantCount, participantCount != 0 { status = .custom(string: strings.Conversation_StatusMembers(participantCount), multiline: false, isActive: false, icon: nil) } else { status = .custom(string: strings.Group_Status, multiline: false, isActive: false, icon: nil) } } else { if let participantCount = participantCount, participantCount != 0 { status = .custom(string: strings.Conversation_StatusSubscribers(participantCount), multiline: false, isActive: false, icon: nil) } else { status = .custom(string: strings.Channel_Status, multiline: false, isActive: false, icon: nil) } } } else { status = .none } } itemPeer = .peer(peer: EnginePeer(peer), chatPeer: EnginePeer(peer)) case let .deviceContact(id, contact): status = .none itemPeer = .deviceContact(stableId: id, contact: contact) } if isSearch { status = .none } var itemContextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? if isContextActionEnabled, let contextAction = interaction.contextAction { itemContextAction = { node, gesture, location in switch itemPeer { case let .peer(peer, _): if let peer = peer { contextAction(peer, node, gesture, location, storyData != nil) } case .deviceContact: break case .thread: break } } } var additionalActions: [ContactsPeerItemAction] = [] if hasMoreButton { additionalActions = [ContactsPeerItemAction(icon: .more, action: { _, sourceNode, gesture in interaction.openPeer(peer, .more, sourceNode, gesture) })] } else if displayCallIcons { additionalActions = [ContactsPeerItemAction(icon: .voiceCall, action: { _, sourceNode, gesture in interaction.openPeer(peer, .voiceCall, sourceNode, gesture) }), ContactsPeerItemAction(icon: .videoCall, action: { _, sourceNode, gesture in interaction.openPeer(peer, .videoCall, sourceNode, gesture) })] } var storyStats: (total: Int, unseen: Int, hasUnseenCloseFriends: Bool)? if let storyData = storyData { storyStats = (storyData.count, storyData.unseenCount, storyData.hasUnseenCloseFriends) let text: String text = presentationData.strings.ChatList_ArchiveStoryCount(Int32(storyData.count)) status = .custom(string: text, multiline: false, isActive: false, icon: nil) } return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in interaction.openPeer(peer, .generic, nil, nil) }, disabledAction: { _ in if case let .peer(peer, _, _) = peer { interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) } }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: storyStats, openStories: { peer, sourceNode in if case let .peer(peerValue, _) = peer, let peerValue { interaction.openStories(peerValue, sourceNode) } }) } } 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 .sort(lhsTheme, lhsStrings, lhsSortOrder): if case let .sort(rhsTheme, rhsStrings, rhsSortOrder) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsSortOrder == rhsSortOrder { return true } else { return false } case let .permissionInfo(lhsTheme, lhsTitle, lhsText, lhsSuppressed): if case let .permissionInfo(rhsTheme, rhsTitle, rhsText, rhsSuppressed) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText, lhsSuppressed == rhsSuppressed { return true } else { return false } case let .permissionEnable(lhsTheme, lhsText): if case let .permissionEnable(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .option(lhsIndex, lhsOption, lhsHeader, lhsTheme, lhsStrings): if case let .option(rhsIndex, rhsOption, rhsHeader, rhsTheme, rhsStrings) = rhs, lhsIndex == rhsIndex, lhsOption == rhsOption, lhsHeader?.id == rhsHeader?.id, lhsTheme === rhsTheme, lhsStrings === rhsStrings { return true } else { return false } case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsDisplayCallIcons, lhsHasMoreButton, lhsEnabled, lhsStoryData, lhsRequiresPremiumForMessaging): switch rhs { case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsDisplayCallIcons, rhsHasMoreButton, rhsEnabled, rhsStoryData, rhsRequiresPremiumForMessaging): if lhsIndex != rhsIndex { return false } if lhsPeer != rhsPeer { return false } if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { if lhsPresence != 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 lhsDisplayCallIcons != rhsDisplayCallIcons { return false } if lhsHasMoreButton != rhsHasMoreButton { return false } if lhsEnabled != rhsEnabled { return false } if lhsStoryData != rhsStoryData { return false } if lhsRequiresPremiumForMessaging != rhsRequiresPremiumForMessaging { return false } return true default: return false } } } static func <(lhs: ContactListNodeEntry, rhs: ContactListNodeEntry) -> Bool { switch lhs { case .search: return true case .sort: switch rhs { case .search: return false default: return true } case .permissionInfo: switch rhs { case .search, .sort: return false default: return true } case .permissionEnable: switch rhs { case .search, .sort, .permissionInfo: return false default: return true } case let .option(lhsIndex, _, _, _, _): switch rhs { case .search, .sort, .permissionInfo, .permissionEnable: return false case let .option(rhsIndex, _, _, _, _): return lhsIndex < rhsIndex case .peer: return true } case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _, _, lhsStoryData, _): switch rhs { case .search, .sort, .permissionInfo, .permissionEnable, .option: return false case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _, _, _, rhsStoryData, _): if (lhsStoryData == nil) != (rhsStoryData == nil) { if lhsStoryData != nil { return true } else { return false } } return lhsIndex < rhsIndex } } } } private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, peerRequiresPremiumForMessaging: [EnginePeer.Id: Bool], peersWithStories: [EnginePeer.Id: PeerStoryStats], authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?, topPeers: [EnginePeer], topPeersPresentation: ContactListPresentation.TopPeers, interaction: ContactListNodeInteraction) -> [ContactListNodeEntry] { var entries: [ContactListNodeEntry] = [] var commonHeader: ListViewItemHeader? var orderedPeers: [ContactListPeer] var headers: [ContactListPeerId: ContactListNameIndexHeader] = [:] var addHeader = false if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { let (suppressed, syncDisabled) = warningSuppressed if !peers.isEmpty && !syncDisabled { let title = strings.Contacts_PermissionsTitle let text = strings.Contacts_PermissionsText switch authorizationStatus { case .denied: entries.append(.permissionInfo(theme, title, text, suppressed)) entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllowInSettings_v0)) addHeader = true case .notDetermined: entries.append(.permissionInfo(theme, title, text, false)) entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllow_v0)) addHeader = true default: break } } } if let storySubscriptions, !storySubscriptions.items.isEmpty { addHeader = true } if addHeader { commonHeader = ChatListSearchItemHeader(type: .text(strings.Contacts_SortedByPresence.uppercased(), AnyHashable(1)), theme: theme, strings: strings, actionTitle: nil, action: nil) } switch presentation { case let .orderedByPresence(options): 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, let rhsPresence = rhsPresence { 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], commonHeader, theme, strings)) } case let .natural(options, _, _): let sortedPeers = peers.sorted(by: { lhs, rhs in let result = EnginePeer.IndexName(lhs.indexName).isLessThan(other: EnginePeer.IndexName(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] = [:] var startsWithLetter: [ContactListPeer] = [] var startsWithOther: [ContactListPeer] = [] let hashHeader = "#".utf16.first! for peer in sortedPeers { var indexHeader: unichar = 35 switch peer.indexName { case let .title(title, _): if let c = title.folding(options: .diacriticInsensitive, locale: .current).uppercased().utf16.first { indexHeader = c } case let .personName(first, last, _, _): switch sortOrder { case .firstLast: if let c = first.folding(options: .diacriticInsensitive, locale: .current).uppercased().utf16.first { indexHeader = c } else if let c = last.folding(options: .diacriticInsensitive, locale: .current).uppercased().utf16.first { indexHeader = c } case .lastFirst: if let c = last.folding(options: .diacriticInsensitive, locale: .current).uppercased().utf16.first { indexHeader = c } else if let c = first.folding(options: .diacriticInsensitive, locale: .current).uppercased().utf16.first { indexHeader = c } } } if let scalar = UnicodeScalar(indexHeader) { if !NSCharacterSet.uppercaseLetters.contains(scalar) { indexHeader = hashHeader startsWithOther.append(peer) } else { startsWithLetter.append(peer) } } else { indexHeader = hashHeader startsWithOther.append(peer) } let header: ContactListNameIndexHeader if let cached = headerCache[indexHeader] { header = cached } else { header = ContactListNameIndexHeader(theme: theme, letter: indexHeader) headerCache[indexHeader] = header } headers[peer.id] = header } for i in 0 ..< options.count { entries.append(.option(i, options[i], nil, theme, strings)) } orderedPeers = startsWithLetter + startsWithOther 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 existingPeerIds = Set() switch topPeersPresentation { case .recent: if !topPeers.isEmpty { let hasDeselectAll = !(selectionState?.selectedPeerIndices ?? [:]).isEmpty let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_FrequentContacts.uppercased(), AnyHashable(hasDeselectAll ? 1 : 0)), theme: theme, strings: strings, actionTitle: hasDeselectAll ? strings.Premium_Gift_ContactSelection_DeselectAll.uppercased() : nil, action: { _ in interaction.deselectAll() }) var index: Int = 0 for peer in topPeers.prefix(15) { if peer.isDeleted { continue } existingPeerIds.insert(.peer(peer.id)) let selection: ContactsPeerItemSelection if let selectionState = selectionState { selection = .selectable(selected: selectionState.selectedPeerIndices[.peer(peer.id)] != nil) } else { selection = .none } let presence = presences[peer.id] entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false)) index += 1 } } case let .custom(sections): if !topPeers.isEmpty { var index: Int = 0 var sectionId: Int = 1 for (title, peerIds, hasActions) in sections { var allSelected = true if let selectedPeerIndices = selectionState?.selectedPeerIndices, !selectedPeerIndices.isEmpty { for peerId in peerIds { if selectedPeerIndices[.peer(peerId)] == nil { allSelected = false break } } } else { allSelected = false } var actionTitle: String? if peerIds.count > 1 { actionTitle = allSelected ? strings.Premium_Gift_ContactSelection_DeselectAll.uppercased() : strings.Premium_Gift_ContactSelection_SelectAll.uppercased() } let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text(title.uppercased(), AnyHashable(10 * sectionId + (allSelected ? 1 : 0))), theme: theme, strings: strings, actionTitle: actionTitle, action: { _ in var existingPeerIds = Set() var peers: [EnginePeer] = [] for peer in topPeers { if !existingPeerIds.contains(peer.id) { if peerIds.contains(peer.id) { peers.append(peer) existingPeerIds.insert(peer.id) } } } interaction.toggleSelection(peers, !allSelected) }) for peerId in peerIds { if let peer = topPeers.first(where: { $0.id == peerId }) { if peer.isDeleted { continue } if existingPeerIds.contains(.peer(peer.id)) { continue } existingPeerIds.insert(.peer(peer.id)) let selection: ContactsPeerItemSelection if let selectionState = selectionState { selection = .selectable(selected: selectionState.selectedPeerIndices[.peer(peer.id)] != nil) } else { selection = .none } let presence = presences[peer.id] entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, hasActions, true, nil, false)) index += 1 } } sectionId += 1 } var hasDeselectAll = !(selectionState?.selectedPeerIndices ?? [:]).isEmpty if !sections.isEmpty, let selectionState { var hasNonBirthdayPeers = false var allBirthdayPeerIds = Set() for (_, peerIds, _) in sections { for peerId in peerIds { allBirthdayPeerIds.insert(peerId) } } for id in selectionState.selectedPeerIndices.keys { if case let .peer(peerId) = id, !allBirthdayPeerIds.contains(peerId) { hasNonBirthdayPeers = true break } } if !hasNonBirthdayPeers { hasDeselectAll = false } } let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_FrequentContacts.uppercased(), AnyHashable(hasDeselectAll ? 1 : 0)), theme: theme, strings: strings, actionTitle: hasDeselectAll ? strings.Premium_Gift_ContactSelection_DeselectAll.uppercased() : nil, action: { _ in interaction.deselectAll() }) for peer in topPeers.prefix(15) { if peer.isDeleted { continue } if existingPeerIds.contains(.peer(peer.id)) { continue } existingPeerIds.insert(.peer(peer.id)) let selection: ContactsPeerItemSelection if let selectionState = selectionState { selection = .selectable(selected: selectionState.selectedPeerIndices[.peer(peer.id)] != nil) } else { selection = .none } let presence = presences[peer.id] entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, peersWithStories[peer.id].flatMap { ContactListNodeEntry.StoryData(count: $0.totalCount, unseenCount: $0.unseenCount, hasUnseenCloseFriends: $0.hasUnseenCloseFriends) }, false)) index += 1 } } case .none: break } if let storySubscriptions { let _ = storySubscriptions /*var index: Int = 0 let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text("HIDDEN STORIES", AnyHashable(0)), theme: theme, strings: strings) for item in storySubscriptions.items { entries.append(.peer(index, .peer(peer: item.peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, ContactListNodeEntry.StoryData(count: item.storyCount, unseenCount: item.unseenCount, hasUnseenCloseFriends: item.hasUnseenCloseFriends))) index += 1 }*/ } var index: Int = 0 if let selectionState = selectionState { for peer in selectionState.foundPeers { if existingPeerIds.contains(peer.id) { continue } existingPeerIds.insert(peer.id) let selection: ContactsPeerItemSelection = .selectable(selected: selectionState.selectedPeerIndices[peer.id] != nil) var presence: EnginePeer.Presence? if case let .peer(peer, _, _) = peer { presence = presences[peer.id] } let enabled: Bool switch peer { case let .peer(peer, _, _): enabled = !disabledPeerIds.contains(peer.id) default: enabled = true } var storyData: ContactListNodeEntry.StoryData? if case let .peer(id) = peer.id { storyData = peersWithStories[id].flatMap { ContactListNodeEntry.StoryData(count: $0.totalCount, unseenCount: $0.unseenCount, hasUnseenCloseFriends: $0.hasUnseenCloseFriends) } } entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, storyData, false)) index += 1 } } for i in 0 ..< orderedPeers.count { let peer = orderedPeers[i] if existingPeerIds.contains(peer.id) { continue } existingPeerIds.insert(peer.id) let selection: ContactsPeerItemSelection if let selectionState = selectionState { selection = .selectable(selected: selectionState.selectedPeerIndices[peer.id] != nil) } else { selection = .none } let header: ListViewItemHeader? switch presentation { case .orderedByPresence: header = commonHeader default: header = headers[peer.id] } var presence: EnginePeer.Presence? if case let .peer(peer, _, _) = peer { presence = presences[peer.id] } var enabled: Bool var requiresPremiumForMessaging = false switch peer { case let .peer(peer, _, _): enabled = !disabledPeerIds.contains(peer.id) if let value = peerRequiresPremiumForMessaging[peer.id], value { requiresPremiumForMessaging = true } if requiresPremiumForMessaging { enabled = false } default: enabled = true } var storyData: ContactListNodeEntry.StoryData? if case let .peer(id) = peer.id { storyData = peersWithStories[id].flatMap { ContactListNodeEntry.StoryData(count: $0.totalCount, unseenCount: $0.unseenCount, hasUnseenCloseFriends: $0.hasUnseenCloseFriends) } } entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, storyData, requiresPremiumForMessaging)) index += 1 } return entries } private func preparedContactListNodeTransition(context: AccountContext, presentationData: PresentationData, from fromEntries: [ContactListNodeEntry], to toEntries: [ContactListNodeEntry], interaction: ContactListNodeInteraction, firstTime: Bool, isEmpty: Bool, generateIndexSections: Bool, animation: ContactListAnimation, isSearch: 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(context: context, presentationData: presentationData, interaction: interaction, isSearch: isSearch), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction, isSearch: isSearch), directionHint: nil) } var shouldFixScroll = false var indexSections: [String] = [] if generateIndexSections { var existingSections = Set() for entry in toEntries { switch entry { case .sort: shouldFixScroll = true case .search: //indexSections.apend(CollectionIndexNode.searchIndex) break case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _, _, _): if let header = header as? ContactListNameIndexHeader { if !existingSections.contains(header.letter) { existingSections.insert(header.letter) if let scalar = UnicodeScalar(header.letter) { let title = "\(Character(scalar))" indexSections.append(title) } } } default: break } } } else { outer: for entry in toEntries { switch entry { case .sort: shouldFixScroll = true break outer default: break } } } var scrollToItem: ListViewScrollToItem? if firstTime && shouldFixScroll && toEntries.count >= 1 { scrollToItem = ListViewScrollToItem(index: 0, position: .top(-50.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) } return ContactsListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, indexSections: indexSections, firstTime: firstTime, isEmpty: isEmpty, scrollToItem: scrollToItem, animation: animation) } private struct ContactsListNodeTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let indexSections: [String] let firstTime: Bool let isEmpty: Bool let scrollToItem: ListViewScrollToItem? let animation: ContactListAnimation } public enum ContactListPresentation { public struct Search { public var signal: Signal public var searchChatList: Bool public var searchDeviceContacts: Bool public var searchGroups: Bool public var searchChannels: Bool public var globalSearch: Bool public var displaySavedMessages: Bool public init(signal: Signal, searchChatList: Bool, searchDeviceContacts: Bool, searchGroups: Bool, searchChannels: Bool, globalSearch: Bool, displaySavedMessages: Bool) { self.signal = signal self.searchChatList = searchChatList self.searchDeviceContacts = searchDeviceContacts self.searchGroups = searchGroups self.searchChannels = searchChannels self.globalSearch = globalSearch self.displaySavedMessages = displaySavedMessages } } public enum TopPeers { case none case recent case custom([(title: String, peerIds: [EnginePeer.Id], hasActions: Bool)]) } case orderedByPresence(options: [ContactListAdditionalOption]) case natural(options: [ContactListAdditionalOption], includeChatList: Bool, topPeers: TopPeers) case search(Search) public var sortOrder: ContactsSortOrder? { switch self { case .orderedByPresence: return .presence case .natural: return .natural default: return nil } } } public struct ContactListNodeGroupSelectionState: Equatable { public let selectedPeerIndices: [ContactListPeerId: Int] public let foundPeers: [ContactListPeer] public let selectedPeerMap: [ContactListPeerId: ContactListPeer] public let nextSelectionIndex: Int private init(selectedPeerIndices: [ContactListPeerId: Int], foundPeers: [ContactListPeer], selectedPeerMap: [ContactListPeerId: ContactListPeer], nextSelectionIndex: Int) { self.selectedPeerIndices = selectedPeerIndices self.foundPeers = foundPeers self.selectedPeerMap = selectedPeerMap self.nextSelectionIndex = nextSelectionIndex } public init() { self.selectedPeerIndices = [:] self.foundPeers = [] self.selectedPeerMap = [:] self.nextSelectionIndex = 0 } public func withToggledPeerId(_ peerId: ContactListPeerId) -> ContactListNodeGroupSelectionState { var updatedIndices = self.selectedPeerIndices if let _ = updatedIndices[peerId] { updatedIndices.removeValue(forKey: peerId) return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, foundPeers: self.foundPeers, selectedPeerMap: self.selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex) } else { updatedIndices[peerId] = self.nextSelectionIndex return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, foundPeers: self.foundPeers, selectedPeerMap: self.selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex + 1) } } public func withFoundPeers(_ foundPeers: [ContactListPeer]) -> ContactListNodeGroupSelectionState { return ContactListNodeGroupSelectionState(selectedPeerIndices: self.selectedPeerIndices, foundPeers: foundPeers, selectedPeerMap: self.selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex) } public func withSelectedPeerMap(_ selectedPeerMap: [ContactListPeerId: ContactListPeer]) -> ContactListNodeGroupSelectionState { return ContactListNodeGroupSelectionState(selectedPeerIndices: self.selectedPeerIndices, foundPeers: self.foundPeers, selectedPeerMap: selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex) } } public final class ContactListNode: ASDisplayNode { private let context: AccountContext private var presentation: ContactListPresentation? private let filters: [ContactListFilter] private let onlyWriteable: Bool public let listNode: ListView private var indexNode: CollectionIndexNode private var indexSections: [String]? private var queuedTransitions: [ContactsListNodeTransition] = [] private var validLayout: (ContainerViewLayout, UIEdgeInsets, CGFloat)? private var _ready = ValuePromise() public var ready: Signal { return self._ready.get() } private var didSetReady = false private let contactPeersViewPromise = Promise<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats])>() let storySubscriptions = Promise(nil) private let selectionStatePromise = Promise(nil) private var selectionStateValue: ContactListNodeGroupSelectionState? { didSet { self.selectionStatePromise.set(.single(self.selectionStateValue)) self.selectionStateUpdated?(self.selectionStateValue) } } public var selectionState: ContactListNodeGroupSelectionState? { return self.selectionStateValue } public var selectionStateSignal: Signal { return self.selectionStatePromise.get() } public var selectionStateUpdated: ((ContactListNodeGroupSelectionState?) -> Void)? public var selectedPeers: [ContactListPeer] { if let selectionState = self.selectionState { var selectedPeers: [ContactListPeer] = [] var selectedIndices: [(Int, ContactListPeerId)] = [] for (id, index) in selectionState.selectedPeerIndices { selectedIndices.append((index, id)) } selectedIndices.sort(by: { lhs, rhs in return lhs.0 < rhs.0 }) for (_, id) in selectedIndices { if let peer = selectionState.selectedPeerMap[id] { selectedPeers.append(peer) } } return selectedPeers } else { return [] } } private let pendingRemovalPeerIdsPromise = ValuePromise>(Set()) private var pendingRemovalPeerIds = Set() { didSet { self.pendingRemovalPeerIdsPromise.set(self.pendingRemovalPeerIds) } } private var interaction: ContactListNodeInteraction? private var enableUpdatesValue = false public var enableUpdates: Bool { get { return self.enableUpdatesValue } set(value) { if value != self.enableUpdatesValue { self.enableUpdatesValue = value let context = self.context let contactsWithPremiumRequired: Signal<[EnginePeer.Id: Bool], NoError> if self.onlyWriteable { contactsWithPremiumRequired = self.context.engine.data.subscribe( TelegramEngine.EngineData.Item.Contacts.List(includePresences: false) ) |> map { contacts -> Set in var result = Set() for peer in contacts.peers { if case let .user(user) = peer, user.flags.contains(.requirePremium) { result.insert(peer.id) } } return result } |> distinctUntilChanged |> mapToSignal { peerIds -> Signal<[EnginePeer.Id: Bool], NoError> in return context.engine.data.subscribe( EngineDataMap( peerIds.map(TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging.init(id:)) ) ) } } else { contactsWithPremiumRequired = .single([:]) } let contactsWithStories: Signal<[EnginePeer.Id: PeerStoryStats], NoError> = self.context.engine.data.subscribe( TelegramEngine.EngineData.Item.Contacts.List(includePresences: false) ) |> map { contacts -> Set in var result = Set() for peer in contacts.peers { result.insert(peer.id) } return result } |> distinctUntilChanged |> mapToSignal { peerIds -> Signal<[EnginePeer.Id: PeerStoryStats], NoError> in return context.engine.data.subscribe( EngineDataMap( peerIds.map(TelegramEngine.EngineData.Item.Peer.StoryStats.init(id:)) ) ) |> map { result -> [EnginePeer.Id: PeerStoryStats] in var filtered: [EnginePeer.Id: PeerStoryStats] = [:] for (id, value) in result { if let value { filtered[id] = value } } return filtered } } if value { self.contactPeersViewPromise.set(combineLatest( self.context.engine.data.subscribe( TelegramEngine.EngineData.Item.Contacts.List(includePresences: true), TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.engine.account.peerId) ), contactsWithPremiumRequired, contactsWithStories ) |> mapToThrottled { next, contactsWithPremiumRequired, contactsWithStories -> Signal<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats]), NoError> in return .single((next.0, next.1, contactsWithPremiumRequired, contactsWithStories)) |> then( .complete() |> delay(5.0, queue: Queue.concurrentDefaultQueue()) ) }) } else { self.contactPeersViewPromise.set(combineLatest(self.context.engine.data.subscribe( TelegramEngine.EngineData.Item.Contacts.List(includePresences: true), TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.engine.account.peerId) ), contactsWithPremiumRequired, contactsWithStories) |> map { next, contactsWithPremiumRequired, contactsWithStories -> (EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats]) in return (next.0, next.1, contactsWithPremiumRequired, contactsWithStories) } |> take(1)) } } } } public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? public var contentScrollingEnded: ((ListView) -> Bool)? public var activateSearch: (() -> Void)? public var openPeer: ((ContactListPeer, ContactListAction, ASDisplayNode?, ContextGesture?) -> Void)? public var openDisabledPeer: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? public var deselectedAll: (() -> Void)? public var updatedSelection: (([EnginePeer], Bool) -> Void)? public var openPrivacyPolicy: (() -> Void)? public var suppressPermissionWarning: (() -> Void)? private let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)? public var openStories: ((EnginePeer, ASDisplayNode) -> Void)? private let previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil) private let disposable = MetaDisposable() private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private let presentationDataPromise: Promise private var authorizationNode: PermissionContentNode private let displayPermissionPlaceholder: Bool public var multipleSelection = false private let isPeerEnabled: ((EnginePeer) -> Bool)? public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, presentation: Signal, filters: [ContactListFilter] = [.excludeSelf], onlyWriteable: Bool, isGroupInvitation: Bool, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, selectionState: ContactListNodeGroupSelectionState? = nil, displayPermissionPlaceholder: Bool = true, displaySortOptions: Bool = false, displayCallIcons: Bool = false, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)? = nil, isSearch: Bool = false, multipleSelection: Bool = false) { self.context = context self.filters = filters self.displayPermissionPlaceholder = displayPermissionPlaceholder self.contextAction = contextAction self.multipleSelection = multipleSelection self.isPeerEnabled = isPeerEnabled self.onlyWriteable = onlyWriteable let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData self.listNode = ListView() self.listNode.dynamicBounceEnabled = false self.listNode.accessibilityPageScrolledString = { row, count in return presentationData.strings.VoiceOver_ScrollStatus(row, count).string } self.indexNode = CollectionIndexNode() self.presentationDataPromise = Promise(self.presentationData) let contactsAuthorization = Promise() contactsAuthorization.set(.single(.allowed) |> then(DeviceAccess.authorizationStatus(subject: .contacts))) let contactsWarningSuppressed = Promise<(Bool, Bool)>() contactsWarningSuppressed.set(.single((false, false)) |> then( combineLatest(context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.permissionWarningKey(permission: .contacts)!), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings])) |> map { noticeView, preferences -> (Bool, Bool) in let settings: ContactsSettings = preferences.values[PreferencesKeys.contactsSettings]?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings let synchronizeDeviceContacts: Bool = settings.synchronizeContacts let suppressed: Bool let timestamp = noticeView.value.flatMap({ ApplicationSpecificNotice.getTimestampValue($0) }) if let timestamp = timestamp, timestamp > 0 { suppressed = true } else { suppressed = false } return (suppressed, !synchronizeDeviceContacts) } )) var authorizeImpl: (() -> Void)? var openPrivacyPolicyImpl: (() -> Void)? self.authorizationNode = PermissionContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, kind: PermissionKind.contacts.rawValue, icon: .image(UIImage(bundleImageName: "Settings/Permissions/Contacts")), title: self.presentationData.strings.Contacts_PermissionsTitle, text: self.presentationData.strings.Contacts_PermissionsText, buttonTitle: self.presentationData.strings.Contacts_PermissionsAllow, buttonAction: { authorizeImpl?() }, openPrivacyPolicy: { openPrivacyPolicyImpl?() }) self.authorizationNode.isHidden = true super.init() self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor self.selectionStateValue = selectionState self.selectionStatePromise.set(.single(selectionState)) self.addSubnode(self.listNode) self.addSubnode(self.indexNode) self.addSubnode(self.authorizationNode) let processingQueue = Queue() let previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil) let previousSelectionState = Atomic(value: nil) let previousPendingRemovalPeerIds = Atomic?>(value: nil) let interaction = ContactListNodeInteraction(activateSearch: { [weak self] in self?.activateSearch?() }, authorize: { authorizeImpl?() }, suppressWarning: { [weak self] in self?.suppressPermissionWarning?() }, openPeer: { [weak self] peer, action, sourceNode, gesture in if let strongSelf = self { if strongSelf.multipleSelection { var updated = false strongSelf.updateSelectionState({ state in if let state = state { updated = true var selectedPeerMap = state.selectedPeerMap selectedPeerMap[peer.id] = peer return state.withToggledPeerId(peer.id).withSelectedPeerMap(selectedPeerMap) } else { return nil } }) if !updated { strongSelf.openPeer?(peer, action, sourceNode, gesture) } } else { strongSelf.openPeer?(peer, action, sourceNode, gesture) } } }, openDisabledPeer: { [weak self] peer, reason in guard let self else { return } self.openDisabledPeer?(peer, reason) }, contextAction: contextAction, openStories: { [weak self] peer, sourceNode in guard let self else { return } self.openStories?(peer, sourceNode) }, deselectAll: { [weak self] in guard let self else { return } self.updateSelectionState({ state in return ContactListNodeGroupSelectionState() }) self.deselectedAll?() }, toggleSelection: { [weak self] peers, value in guard let self = self else { return } self.updateSelectionState({ state in var state = state ?? ContactListNodeGroupSelectionState() var selectedPeerMap = state.selectedPeerMap for peer in peers { let id: ContactListPeerId = .peer(peer.id) if (state.selectedPeerIndices[id] != nil) != value { state = state.withToggledPeerId(.peer(peer.id)) } if value { selectedPeerMap[id] = .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil) } else { selectedPeerMap.removeValue(forKey: id) } } state = state.withSelectedPeerMap(selectedPeerMap) return state }) self.updatedSelection?(peers, value) }) self.indexNode.indexSelected = { [weak self] section in guard let strongSelf = self, let layout = strongSelf.validLayout, let entries = previousEntries.with({ $0 }) else { return } var insets = layout.0.insets(options: [.input]) insets.left = layout.0.safeInsets.left insets.right = layout.0.safeInsets.right var headerInsets = layout.1 if headerInsets.top == insets.top { headerInsets.top -= navigationBarSearchContentHeight } let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.0.size, insets: insets, headerInsets: headerInsets, duration: 0.0, curve: .Default(duration: nil)) var index = 0 var peerIndex = 0 loop: for entry in entries { switch entry { case .search: if section == CollectionIndexNode.searchIndex { strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.PreferSynchronousDrawing, .PreferSynchronousResourceLoading], scrollToItem: ListViewScrollToItem(index: index, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: nil), directionHint: .Down), additionalScrollDistance: 0.0, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) break loop } case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _, _, _): if let header = header as? ContactListNameIndexHeader { if let scalar = UnicodeScalar(header.letter) { let title = "\(Character(scalar))" if title == section { strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.PreferSynchronousDrawing, .PreferSynchronousResourceLoading], scrollToItem: ListViewScrollToItem(index: peerIndex == 0 ? 0 : index, position: .top(peerIndex == 0 ? 0.0 : -navigationBarSearchContentHeight), animated: false, curve: .Default(duration: nil), directionHint: .Down), additionalScrollDistance: 0.0, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) break loop } } } peerIndex += 1 default: break } index += 1 } } self.interaction = interaction let context = self.context var firstTime: Int32 = 1 let selectionStateSignal = self.selectionStatePromise.get() let pendingRemovalPeerIdsSignal = self.pendingRemovalPeerIdsPromise.get() let transition: Signal let presentationDataPromise = self.presentationDataPromise transition = presentation |> mapToSignal { presentation in var generateSections = false var includeChatList = false var displayTopPeers: ContactListPresentation.TopPeers = .none if case let .natural(_, includeChatListValue, topPeersValue) = presentation { generateSections = true includeChatList = includeChatListValue displayTopPeers = topPeersValue } if case let .search(search) = presentation { let query = search.signal let searchChatList = search.searchChatList let searchDeviceContacts = search.searchDeviceContacts let searchGroups = search.searchGroups let searchChannels = search.searchChannels let globalSearch = search.globalSearch let displaySavedMessages = search.displaySavedMessages return query |> mapToSignal { query in let foundLocalContacts: Signal<([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]), NoError> if searchChatList { let foundChatListPeers = context.account.postbox.searchPeers(query: query.lowercased()) foundLocalContacts = foundChatListPeers |> mapToSignal { peers -> Signal<([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]), NoError> in var resultPeers: [FoundPeer] = [] for peer in peers { if !displaySavedMessages { if peer.peerId == context.account.peerId { continue } } if searchGroups || searchChannels { let mainPeer = peer.chatMainPeer if let _ = mainPeer as? TelegramUser { } else if let _ = mainPeer as? TelegramGroup { } else if let channel = mainPeer as? TelegramChannel { if case .broadcast = channel.info { if !searchChannels { continue } } } else { continue } } else { if peer.peerId.namespace != Namespaces.Peer.CloudUser { continue } } if let mainPeer = peer.chatMainPeer { var matches = true if let isPeerEnabled = isPeerEnabled { matches = isPeerEnabled(EnginePeer(mainPeer)) } if matches { resultPeers.append(FoundPeer(peer: mainPeer, subscribers: nil)) } } } return context.engine.data.get( EngineDataMap(resultPeers.map(\.peer.id).map(TelegramEngine.EngineData.Item.Peer.Presence.init)), EngineDataMap(resultPeers.map(\.peer.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) ) |> map { presenceMap, participantCountMap -> ([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]) in var resultPresences: [EnginePeer.Id: EnginePeer.Presence] = [:] var mappedPeers: [FoundPeer] = [] for peer in resultPeers { if let maybePresence = presenceMap[peer.peer.id], let presence = maybePresence { resultPresences[peer.peer.id] = presence } if let _ = peer.peer as? TelegramChannel { var subscribers: Int32? if let maybeMemberCount = participantCountMap[peer.peer.id], let memberCount = maybeMemberCount { subscribers = Int32(memberCount) } mappedPeers.append(FoundPeer(peer: peer.peer, subscribers: subscribers)) } else { mappedPeers.append(peer) } } return (mappedPeers, resultPresences) } } } else { foundLocalContacts = context.engine.contacts.searchContacts(query: query.lowercased()) |> map { peers, presences -> ([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]) in return (peers.map({ FoundPeer(peer: $0._asPeer(), subscribers: nil) }), presences) } } var foundRemoteContacts: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], [])) if globalSearch { foundRemoteContacts = foundRemoteContacts |> then( context.engine.contacts.searchRemotePeers(query: query) |> map { ($0.0, $0.1) } |> delay(0.2, queue: Queue.concurrentDefaultQueue()) ) } let foundDeviceContacts: Signal<[DeviceContactStableId: (DeviceContactBasicData, EnginePeer.Id?)], NoError> if searchDeviceContacts { foundDeviceContacts = context.sharedContext.contactDataManager?.search(query: query) ?? .single([:]) } else { foundDeviceContacts = .single([:]) } let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1) struct FoundPeers { var foundLocalContacts: ([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]) var foundRemoteContacts: ([FoundPeer], [FoundPeer]) } let foundPeers = Promise() foundPeers.set(combineLatest( foundLocalContacts, foundRemoteContacts ) |> map { foundLocalContacts, foundRemoteContacts -> FoundPeers in return FoundPeers( foundLocalContacts: foundLocalContacts, foundRemoteContacts: foundRemoteContacts ) }) let peerRequiresPremiumForMessaging: Signal<[EnginePeer.Id: Bool], NoError> if onlyWriteable && !isGroupInvitation { peerRequiresPremiumForMessaging = foundPeers.get() |> map { foundPeers -> Set in var result = Set() for peer in foundPeers.foundLocalContacts.0 { if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) { result.insert(user.id) } } for peer in foundPeers.foundRemoteContacts.0 { if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) { result.insert(user.id) } } for peer in foundPeers.foundRemoteContacts.1 { if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) { result.insert(user.id) } } return result } |> distinctUntilChanged |> mapToSignal { peerIds -> Signal<[EnginePeer.Id: Bool], NoError> in return context.engine.data.subscribe( EngineDataMap( peerIds.map(TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging.init(id:)) ) ) } } else { peerRequiresPremiumForMessaging = .single([:]) } return combineLatest(accountPeer, foundPeers.get(), peerRequiresPremiumForMessaging, foundDeviceContacts, selectionStateSignal, pendingRemovalPeerIdsSignal, presentationDataPromise.get()) |> mapToQueue { accountPeer, foundPeers, peerRequiresPremiumForMessaging, deviceContacts, selectionState, pendingRemovalPeerIds, presentationData -> Signal in let localPeersAndStatuses = foundPeers.foundLocalContacts let remotePeers = foundPeers.foundRemoteContacts let signal = deferred { () -> Signal in if !peerRequiresPremiumForMessaging.isEmpty { context.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: Array(peerRequiresPremiumForMessaging.keys)) } var existingPeerIds = Set() var disabledPeerIds = Set() var existingNormalizedPhoneNumbers = Set() var excludeSelf = false var requirePhoneNumbers = false for filter in filters { switch filter { case .excludeSelf: excludeSelf = true existingPeerIds.insert(context.account.peerId) case let .exclude(peerIds): existingPeerIds = existingPeerIds.union(peerIds) case let .disable(peerIds): disabledPeerIds = disabledPeerIds.union(peerIds) case .excludeWithoutPhoneNumbers: requirePhoneNumbers = true case .excludeBots: break } } var peers: [ContactListPeer] = [] if let selectionState = selectionState { for peer in selectionState.foundPeers { if case let .peer(peer, _, _) = peer { existingPeerIds.insert(peer.id) } peers.append(peer) } } if !excludeSelf && !existingPeerIds.contains(accountPeer.id) { let lowercasedQuery = query.lowercased() if presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) { existingPeerIds.insert(accountPeer.id) peers.append(.peer(peer: accountPeer, isGlobal: false, participantCount: nil)) } } for peer in localPeersAndStatuses.0 { if existingPeerIds.contains(peer.peer.id) || pendingRemovalPeerIds.contains(peer.peer.id) { continue } existingPeerIds.insert(peer.peer.id) peers.append(.peer(peer: peer.peer, isGlobal: false, participantCount: peer.subscribers)) if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } } for peer in remotePeers.0 { let matches: Bool if let user = peer.peer as? TelegramUser { let phone = user.phone ?? "" if requirePhoneNumbers && phone.isEmpty { matches = false } else { matches = true } } else if searchGroups || searchChannels { if peer.peer is TelegramGroup && searchGroups { matches = true } else if let channel = peer.peer as? TelegramChannel { if case .group = channel.info { matches = searchGroups } else { matches = searchChannels } } else { matches = false } } else { matches = false } if matches { if existingPeerIds.contains(peer.peer.id) || pendingRemovalPeerIds.contains(peer.peer.id) { continue } existingPeerIds.insert(peer.peer.id) peers.append(.peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers)) if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } } } for peer in remotePeers.1 { let matches: Bool if let user = peer.peer as? TelegramUser { let phone = user.phone ?? "" if requirePhoneNumbers && phone.isEmpty { matches = false } else { matches = true } } else if searchGroups || searchChannels { if peer.peer is TelegramGroup { matches = searchGroups } else if let channel = peer.peer as? TelegramChannel { if case .group = channel.info { matches = searchGroups } else { matches = searchChannels } } else { matches = false } } else { matches = false } if matches { if existingPeerIds.contains(peer.peer.id) || pendingRemovalPeerIds.contains(peer.peer.id) { continue } existingPeerIds.insert(peer.peer.id) peers.append(.peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers)) 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.0.phoneNumbers { let normalizedNumber = DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phoneNumber.value)) if existingNormalizedPhoneNumbers.contains(normalizedNumber) { continue outer } } if let peerId = contact.1 { if existingPeerIds.contains(peerId) { continue outer } } peers.append(.deviceContact(stableId, contact.0)) } let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: peerRequiresPremiumForMessaging, peersWithStories: [:], authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil, topPeers: [], topPeersPresentation: .none, interaction: interaction) let previous = previousEntries.swap(entries) return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch)) } if OSAtomicCompareAndSwap32(1, 0, &firstTime) { return signal |> runOn(Queue.mainQueue()) } else { return signal |> runOn(processingQueue) } } } } else { let chatListSignal: Signal<[(EnginePeer, Int32)], NoError> if includeChatList { chatListSignal = self.context.account.viewTracker.tailChatListView(groupId: .root, count: 100) |> take(1) |> mapToSignal { view, _ -> Signal<[(EnginePeer, Int32)], NoError> in return context.engine.data.get(EngineDataMap( view.entries.compactMap { entry -> EnginePeer.Id? in switch entry { case let .MessageEntry(entryData): if let peer = entryData.renderedPeer.peer { if let channel = peer as? TelegramChannel, case .group = channel.info { return peer.id } } default: break } return nil }.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init) )) |> map { participantCountMap -> [(EnginePeer, Int32)] in var peers: [(EnginePeer, Int32)] = [] for entry in view.entries { switch entry { case let .MessageEntry(entryData): if let peer = entryData.renderedPeer.peer { if peer is TelegramGroup { peers.append((EnginePeer(peer), 0)) } else if let channel = peer as? TelegramChannel, case .group = channel.info { var memberCount: Int32 = 0 if let maybeParticipantCount = participantCountMap[peer.id], let participantCount = maybeParticipantCount { memberCount = Int32(participantCount) } peers.append((EnginePeer(peer), memberCount)) } } default: break } } return peers } } } else { chatListSignal = .single([]) } struct TopPeer { let peer: EnginePeer let presence: EnginePeer.Presence? } let topPeers: Signal<[TopPeer], NoError> switch displayTopPeers { case .recent: topPeers = context.engine.peers.recentPeers() |> mapToSignal { recentPeers -> Signal<[TopPeer], NoError> in if case let .peers(peers) = recentPeers { let topPeers = peers.map(EnginePeer.init) return context.engine.data.subscribe( EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.Presence.init)) ) |> map { presences -> [TopPeer] in var result: [TopPeer] = [] for peer in topPeers { var presence: EnginePeer.Presence? if let maybePresence = presences[peer.id], let presenceValue = maybePresence { presence = presenceValue } result.append(TopPeer(peer: peer, presence: presence)) } return result } } else { return .single([]) } } case let .custom(sections): var peerIds: [EnginePeer.Id] = [] for (_, sectionPeers, _) in sections { peerIds.append(contentsOf: sectionPeers) } topPeers = combineLatest( context.engine.data.get(EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))), context.engine.peers.recentPeers() |> mapToSignal { recentPeers -> Signal<[TopPeer], NoError> in if case let .peers(peers) = recentPeers { let topPeers = peers.map(EnginePeer.init) return context.engine.data.subscribe( EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.Presence.init)) ) |> map { presences -> [TopPeer] in var result: [TopPeer] = [] for peer in topPeers { var presence: EnginePeer.Presence? if let maybePresence = presences[peer.id], let presenceValue = maybePresence { presence = presenceValue } result.append(TopPeer(peer: peer, presence: presence)) } return result } } else { return .single([]) } } ) |> map { peers, recentPeers in var result: [TopPeer] = [] for peer in peers.values { if let peer { result.append(TopPeer(peer: peer, presence: nil)) } } result.append(contentsOf: recentPeers) return result } case .none: topPeers = .single([]) } return (combineLatest( self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, pendingRemovalPeerIdsSignal, presentationDataPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get(), self.storySubscriptions.get(), topPeers ) |> mapToQueue { view, chatListPeers, selectionState, pendingRemovalPeerIds, presentationData, authorizationStatus, warningSuppressed, storySubscriptions, topPeers -> Signal in let signal = deferred { () -> Signal in if !view.2.isEmpty { context.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: Array(view.2.keys)) } var peers = view.0.peers.map({ ContactListPeer.peer(peer: $0._asPeer(), isGlobal: false, participantCount: nil) }) for (peer, memberCount) in chatListPeers { peers.append(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: memberCount)) } var existingPeerIds = Set() var disabledPeerIds = Set() var requirePhoneNumbers = false for filter in filters { switch filter { case .excludeSelf: existingPeerIds.insert(context.account.peerId) case let .exclude(peerIds): existingPeerIds = existingPeerIds.union(peerIds) case let .disable(peerIds): disabledPeerIds = disabledPeerIds.union(peerIds) case .excludeWithoutPhoneNumbers: requirePhoneNumbers = true case .excludeBots: break } } peers = peers.filter { contact in switch contact { case let .peer(peer, _, _): if requirePhoneNumbers, let user = peer as? TelegramUser { let phone = user.phone ?? "" if phone.isEmpty { return false } } return !existingPeerIds.contains(peer.id) && !pendingRemovalPeerIds.contains(peer.id) default: return true } } var presences = view.0.presences for peer in topPeers { if let presence = peer.presence { presences[peer.peer.id] = presence } } var isEmpty = false if (authorizationStatus == .notDetermined || authorizationStatus == .denied) && peers.isEmpty { isEmpty = true } let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: view.2, peersWithStories: view.3, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions, topPeers: topPeers.map { $0.peer }, topPeersPresentation: displayTopPeers, interaction: interaction) let previous = previousEntries.swap(entries) let previousSelection = previousSelectionState.swap(selectionState) let previousPendingRemovalPeerIds = previousPendingRemovalPeerIds.swap(pendingRemovalPeerIds) var hadPermissionInfo = false var previousOptionsCount = 0 if let previous = previous { for entry in previous { if case .permissionInfo = entry { hadPermissionInfo = true } if case .option = entry { previousOptionsCount += 1 } } } var hasPermissionInfo = false var optionsCount = 0 for entry in entries { if case .permissionInfo = entry { hasPermissionInfo = true } if case .option = entry { optionsCount += 1 } } let animation: ContactListAnimation if (previousSelection == nil) != (selectionState == nil) { animation = .insertion } else if previousPendingRemovalPeerIds != pendingRemovalPeerIds { animation = .insertion } else if hadPermissionInfo != hasPermissionInfo { animation = .insertion } else if optionsCount < previousOptionsCount { animation = .insertion } else { animation = .none } return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: isEmpty, generateIndexSections: generateSections, animation: animation, isSearch: isSearch)) } if OSAtomicCompareAndSwap32(1, 0, &firstTime) { return signal |> runOn(Queue.mainQueue()) } else { return signal |> runOn(processingQueue) } }) |> deliverOnMainQueue } } self.disposable.set((transition |> deliverOnMainQueue).start(next: { [weak self] transition in self?.enqueueTransition(transition) })) self.presentationDataDisposable = ((updatedPresentationData?.signal ?? 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.backgroundColor = presentationData.theme.chatList.backgroundColor strongSelf.listNode.verticalScrollIndicatorColor = presentationData.theme.list.scrollIndicatorColor strongSelf.presentationDataPromise.set(.single(presentationData)) let authorizationPreviousHidden = strongSelf.authorizationNode.isHidden strongSelf.authorizationNode.removeFromSupernode() strongSelf.authorizationNode = PermissionContentNode(context: strongSelf.context, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, kind: PermissionKind.contacts.rawValue, icon: .image(UIImage(bundleImageName: "Settings/Permissions/Contacts")), title: strongSelf.presentationData.strings.Contacts_PermissionsTitle, text: strongSelf.presentationData.strings.Contacts_PermissionsText, buttonTitle: strongSelf.presentationData.strings.Contacts_PermissionsAllow, buttonAction: { authorizeImpl?() }, openPrivacyPolicy: { openPrivacyPolicyImpl?() }) strongSelf.authorizationNode.isHidden = authorizationPreviousHidden strongSelf.addSubnode(strongSelf.authorizationNode) strongSelf.listNode.dynamicBounceEnabled = false 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) } }) if let (validLayout, headerInsets, storiesInset) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(validLayout, headerInsets: headerInsets, storiesInset: storiesInset, transition: .immediate) } } } }).strict() self.listNode.didEndScrolling = { [weak self] _ in if let strongSelf = self { let _ = strongSelf.contentScrollingEnded?(strongSelf.listNode) } } self.listNode.visibleContentOffsetChanged = { [weak self] offset in if let strongSelf = self { strongSelf.contentOffsetChanged?(offset) } } authorizeImpl = { let _ = (DeviceAccess.authorizationStatus(subject: .contacts) |> take(1) |> deliverOnMainQueue).start(next: { status in switch status { case .notDetermined: DeviceAccess.authorizeAccess(to: .contacts) case .denied, .restricted: context.sharedContext.applicationBindings.openSettings() default: break } }) } openPrivacyPolicyImpl = { [weak self] in self?.openPrivacyPolicy?() } self.enableUpdates = true } public func updateSelectedChatLocation(_ chatLocation: ChatLocation?, progress: CGFloat, transition: ContainedViewLayoutTransition) { self.interaction?.itemHighlighting.chatLocation = chatLocation self.interaction?.itemHighlighting.progress = progress self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ContactsPeerItemNode { itemNode.updateIsHighlighted(transition: transition) } } } deinit { self.disposable.dispose() self.presentationDataDisposable?.dispose() } public func updateSelectionState(_ f: (ContactListNodeGroupSelectionState?) -> ContactListNodeGroupSelectionState?) { let updatedSelectionState = f(self.selectionStateValue) if updatedSelectionState != self.selectionStateValue { self.selectionStateValue = updatedSelectionState } } public func updatePendingRemovalPeerIds(_ f: (Set) -> Set) { let updatedPendingRemovalPeerIds = f(self.pendingRemovalPeerIds) if updatedPendingRemovalPeerIds != self.pendingRemovalPeerIds { self.pendingRemovalPeerIds = updatedPendingRemovalPeerIds } } private var previousStoriesInset: CGFloat? public var ignoreStoryInsetAdjustment: Bool = false public func containerLayoutUpdated(_ layout: ContainerViewLayout, headerInsets: UIEdgeInsets, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) { let hadValidLayout = self.validLayout != nil self.validLayout = (layout, headerInsets, storiesInset) var insets = layout.insets(options: [.input]) insets.left = layout.safeInsets.left insets.right = layout.safeInsets.right var headerInsets = headerInsets if !hadValidLayout { headerInsets.top -= navigationBarSearchContentHeight } var additionalScrollDistance: CGFloat = 0.0 if let previousStoriesInset = self.previousStoriesInset { if self.ignoreStoryInsetAdjustment { } else { additionalScrollDistance += previousStoriesInset - storiesInset } } self.previousStoriesInset = storiesInset transition.updateFrame(node: self.listNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if let indexSections = self.indexSections { var insets = layout.insets(options: [.input]) if let inputHeight = layout.inputHeight { insets.bottom -= inputHeight } insets.left = layout.safeInsets.left insets.right = layout.safeInsets.right let indexNodeFrame = CGRect(origin: CGPoint(x: layout.size.width - insets.right - 20.0, y: insets.top), size: CGSize(width: 20.0, height: layout.size.height - insets.top - insets.bottom)) transition.updateFrame(node: indexNode, frame: indexNodeFrame) self.indexNode.update(size: indexNodeFrame.size, color: self.presentationData.theme.list.itemAccentColor, sections: indexSections, transition: transition) } if self.multipleSelection { let permissionSize = CGSize(width: layout.size.width, height: layout.size.height - 160.0) var permissionInsets = insets permissionInsets.bottom += 100.0 self.authorizationNode.updateLayout(size: permissionSize, insets: permissionInsets, transition: transition) } else { self.authorizationNode.updateLayout(size: layout.size, insets: insets, transition: transition) } transition.updateFrame(node: self.authorizationNode, frame: self.bounds) if !hadValidLayout { self.dequeueTransitions() } } private func enqueueTransition(_ transition: ContactsListNodeTransition) { self.queuedTransitions.append(transition) if self.validLayout != nil { self.dequeueTransitions() } } private func dequeueTransitions() { if self.validLayout != nil { while !self.queuedTransitions.isEmpty { let transition = self.queuedTransitions.removeFirst() var options = ListViewDeleteAndInsertOptions() if transition.firstTime { options.insert(.Synchronous) options.insert(.LowLatency) options.insert(.PreferSynchronousDrawing) options.insert(.PreferSynchronousResourceLoading) } else if transition.animation != .none { if transition.animation == .insertion { options.insert(.AnimateInsertion) } else if let presentation = self.presentation, case .orderedByPresence = presentation { options.insert(.AnimateCrossfade) } } if let (layout, _, _) = self.validLayout { self.indexSections = transition.indexSections var insets = layout.insets(options: [.input]) insets.left = layout.safeInsets.left insets.right = layout.safeInsets.right if let inputHeight = layout.inputHeight { insets.bottom -= inputHeight } let indexNodeFrame = CGRect(origin: CGPoint(x: layout.size.width - insets.right - 20.0, y: insets.top), size: CGSize(width: 20.0, height: layout.size.height - insets.top - insets.bottom)) self.indexNode.frame = indexNodeFrame self.indexNode.update(size: CGSize(width: 20.0, height: layout.size.height - insets.top - insets.bottom), color: self.presentationData.theme.list.itemAccentColor, sections: transition.indexSections, transition: .animated(duration: 0.2, curve: .easeInOut)) self.indexNode.isUserInteractionEnabled = !transition.indexSections.isEmpty } self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: transition.scrollToItem, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self { if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) } } }) self.listNode.isHidden = self.displayPermissionPlaceholder && transition.isEmpty self.authorizationNode.isHidden = !transition.isEmpty || !self.displayPermissionPlaceholder } } } 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 }) } }