import Foundation import SwiftSignalKit import TelegramCore import AccountContext import TelegramUIPreferences import TemporaryCachedPeerDataManager import Postbox public extension ShareWithPeersScreen { final class State { let sendAsPeers: [EnginePeer] let peers: [EnginePeer] let peersMap: [EnginePeer.Id: EnginePeer] let savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] let presences: [EnginePeer.Id: EnginePeer.Presence] let invitedAt: [EnginePeer.Id: Int32] let participants: [EnginePeer.Id: Int] let closeFriendsPeers: [EnginePeer] let grayListPeers: [EnginePeer] fileprivate init( sendAsPeers: [EnginePeer] = [], peers: [EnginePeer], peersMap: [EnginePeer.Id: EnginePeer] = [:], savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:], presences: [EnginePeer.Id: EnginePeer.Presence] = [:], invitedAt: [EnginePeer.Id: Int32] = [:], participants: [EnginePeer.Id: Int] = [:], closeFriendsPeers: [EnginePeer] = [], grayListPeers: [EnginePeer] = [] ) { self.sendAsPeers = sendAsPeers self.peers = peers self.peersMap = peersMap self.savedSelectedPeers = savedSelectedPeers self.presences = presences self.invitedAt = invitedAt self.participants = participants self.closeFriendsPeers = closeFriendsPeers self.grayListPeers = grayListPeers } } final class StateContext { public enum Subject: Equatable { case peers(peers: [EnginePeer], peerId: EnginePeer.Id?) case stories(editing: Bool) case chats(blocked: Bool) case contacts(base: EngineStoryPrivacy.Base) case contactsSearch(query: String, onlyContacts: Bool) case members(isGroup: Bool, peerId: EnginePeer.Id, searchQuery: String?) case channels(isGroup: Bool, exclude: Set, searchQuery: String?) } var stateValue: State? public let subject: Subject public let editing: Bool public private(set) var initialPeerIds: Set = Set() let blockedPeersContext: BlockedPeersContext? private var stateDisposable: Disposable? private let stateSubject = Promise() public var state: Signal { return self.stateSubject.get() } private var listControl: PeerChannelMemberCategoryControl? private let readySubject = ValuePromise(false, ignoreRepeated: true) public var ready: Signal { return self.readySubject.get() } public init( context: AccountContext, subject: Subject = .chats(blocked: false), editing: Bool = false, initialSelectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:], initialPeerIds: Set = Set(), closeFriends: Signal<[EnginePeer], NoError> = .single([]), adminedChannels: Signal<[EnginePeer], NoError> = .single([]), blockedPeersContext: BlockedPeersContext? = nil ) { self.subject = subject self.editing = editing self.initialPeerIds = initialPeerIds self.blockedPeersContext = blockedPeersContext let grayListPeers: Signal<[EnginePeer], NoError> if let blockedPeersContext { grayListPeers = blockedPeersContext.state |> map { state -> [EnginePeer] in return state.peers.compactMap { $0.peer.flatMap(EnginePeer.init) } } } else { grayListPeers = .single([]) } switch subject { case let .peers(peers, _): self.stateDisposable = (.single(peers) |> mapToSignal { peers -> Signal<([EnginePeer], [EnginePeer.Id: Optional]), NoError> in return context.engine.data.subscribe( EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) ) |> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional]) in return (peers, participantCountMap) } } |> deliverOnMainQueue).start(next: { [weak self] peers, participantCounts in guard let self else { return } var participants: [EnginePeer.Id: Int] = [:] for (key, value) in participantCounts { if let value { participants[key] = value } } let state = State( sendAsPeers: peers, peers: [], participants: participants ) self.stateValue = state self.stateSubject.set(.single(state)) self.readySubject.set(true) }) case .stories: let savedEveryoneExceptionPeers = peersListStoredState(engine: context.engine, base: .everyone) let savedContactsExceptionPeers = peersListStoredState(engine: context.engine, base: .contacts) let savedSelectedPeers = peersListStoredState(engine: context.engine, base: .nobody) let savedPeers = combineLatest( savedEveryoneExceptionPeers, savedContactsExceptionPeers, savedSelectedPeers ) |> mapToSignal { everyone, contacts, selected -> Signal<([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]), NoError> in var everyone = everyone if let initialPeerIds = initialSelectedPeers[.everyone] { everyone = initialPeerIds } var everyonePeerSignals: [Signal] = [] if everyone.count < 3 { for peerId in everyone { everyonePeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) } } var contacts = contacts if let initialPeerIds = initialSelectedPeers[.contacts] { contacts = initialPeerIds } var contactsPeerSignals: [Signal] = [] if contacts.count < 3 { for peerId in contacts { contactsPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) } } var selected = selected if let initialPeerIds = initialSelectedPeers[.nobody] { selected = initialPeerIds } var selectedPeerSignals: [Signal] = [] if selected.count < 3 { for peerId in selected { selectedPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) } } return combineLatest( combineLatest(everyonePeerSignals), combineLatest(contactsPeerSignals), combineLatest(selectedPeerSignals) ) |> map { everyonePeers, contactsPeers, selectedPeers -> ([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]) in var peersMap: [EnginePeer.Id: EnginePeer] = [:] for peer in everyonePeers { if let peer { peersMap[peer.id] = peer } } for peer in contactsPeers { if let peer { peersMap[peer.id] = peer } } for peer in selectedPeers { if let peer { peersMap[peer.id] = peer } } return ( peersMap, everyone, contacts, selected ) } } let adminedChannelsWithParticipants = adminedChannels |> mapToSignal { peers -> Signal<([EnginePeer], [EnginePeer.Id: Optional]), NoError> in return context.engine.data.subscribe( EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) ) |> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional]) in return (peers, participantCountMap) } } self.stateDisposable = combineLatest( queue: Queue.mainQueue(), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), adminedChannelsWithParticipants, savedPeers, closeFriends, grayListPeers ) .start(next: { [weak self] accountPeer, adminedChannelsWithParticipants, savedPeers, closeFriends, grayListPeers in guard let self else { return } let (adminedChannels, participantCounts) = adminedChannelsWithParticipants var participants: [EnginePeer.Id: Int] = [:] for (key, value) in participantCounts { if let value { participants[key] = value } } var sendAsPeers: [EnginePeer] = [] if let accountPeer { sendAsPeers.append(accountPeer) } for channel in adminedChannels { if case let .channel(channel) = channel, channel.hasPermission(.postStories) { if !sendAsPeers.contains(where: { $0.id == channel.id }) { sendAsPeers.append(contentsOf: adminedChannels) } } } let (peersMap, everyonePeers, contactsPeers, selectedPeers) = savedPeers var savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:] savedSelectedPeers[.everyone] = everyonePeers savedSelectedPeers[.contacts] = contactsPeers savedSelectedPeers[.nobody] = selectedPeers let state = State( sendAsPeers: sendAsPeers, peers: [], peersMap: peersMap, savedSelectedPeers: savedSelectedPeers, participants: participants, closeFriendsPeers: closeFriends, grayListPeers: grayListPeers ) self.stateValue = state self.stateSubject.set(.single(state)) self.readySubject.set(true) }) case let .chats(isGrayList): self.stateDisposable = (combineLatest( context.engine.messages.chatList(group: .root, count: 200) |> take(1), context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)), context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init))), grayListPeers ) |> mapToSignal { chatList, contacts, initialPeers, grayListPeers -> Signal<(EngineChatList, EngineContactList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional], [EnginePeer]), NoError> in return context.engine.data.subscribe( EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) ) |> map { participantCountMap -> (EngineChatList, EngineContactList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional], [EnginePeer]) in return (chatList, contacts, initialPeers, participantCountMap, grayListPeers) } } |> deliverOnMainQueue).start(next: { [weak self] chatList, contacts, initialPeers, participantCounts, grayListPeers in guard let self else { return } var participants: [EnginePeer.Id: Int] = [:] for (key, value) in participantCounts { if let value { participants[key] = value } } var grayListPeersIds = Set() for peer in grayListPeers { grayListPeersIds.insert(peer.id) } var existingIds = Set() var selectedPeers: [EnginePeer] = [] if isGrayList { self.initialPeerIds = Set(grayListPeers.map { $0.id }) } for item in chatList.items.reversed() { if let peer = item.renderedPeer.peer { if self.initialPeerIds.contains(peer.id) || isGrayList && grayListPeersIds.contains(peer.id) { selectedPeers.append(peer) existingIds.insert(peer.id) } } } for peerId in self.initialPeerIds { if !existingIds.contains(peerId), let maybePeer = initialPeers[peerId], let peer = maybePeer { selectedPeers.append(peer) existingIds.insert(peerId) } } if isGrayList { for peer in grayListPeers { if !existingIds.contains(peer.id) { selectedPeers.append(peer) existingIds.insert(peer.id) } } } var presences: [EnginePeer.Id: EnginePeer.Presence] = [:] for item in chatList.items { presences[item.renderedPeer.peerId] = item.presence } var peers: [EnginePeer] = [] peers = chatList.items.filter { peer in if let peer = peer.renderedPeer.peer { if case .secretChat = peer { return false } if self.initialPeerIds.contains(peer.id) { return false } if peer.id == context.account.peerId { return false } if peer.isService || peer.isDeleted { return false } if case let .user(user) = peer { if user.botInfo != nil { return false } } if case let .channel(channel) = peer { if channel.isForum { return false } if case .broadcast = channel.info { return false } } return true } else { return false } }.reversed().compactMap { $0.renderedPeer.peer } for peer in peers { existingIds.insert(peer.id) } peers.insert(contentsOf: selectedPeers, at: 0) let state = State( peers: peers, presences: presences, participants: participants, grayListPeers: grayListPeers ) self.stateValue = state self.stateSubject.set(.single(state)) self.readySubject.set(true) }) case let .contacts(base): self.stateDisposable = (context.engine.data.subscribe( TelegramEngine.EngineData.Item.Contacts.List(includePresences: true) ) |> deliverOnMainQueue).start(next: { [weak self] contactList in guard let self else { return } var selectedPeers: [EnginePeer] = [] if case .closeFriends = base { for peer in contactList.peers { if case let .user(user) = peer, user.flags.contains(.isCloseFriend) { selectedPeers.append(peer) } } self.initialPeerIds = Set(selectedPeers.map { $0.id }) } else { for peer in contactList.peers { if case let .user(user) = peer, initialPeerIds.contains(user.id), !user.isDeleted { selectedPeers.append(peer) } } self.initialPeerIds = initialPeerIds } selectedPeers = selectedPeers.sorted(by: { lhs, rhs in let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast) if result == .orderedSame { return lhs.id < rhs.id } else { return result == .orderedAscending } }) var peers: [EnginePeer] = [] peers = contactList.peers.filter { !self.initialPeerIds.contains($0.id) && $0.id != context.account.peerId && !$0.isDeleted }.sorted(by: { lhs, rhs in let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast) if result == .orderedSame { return lhs.id < rhs.id } else { return result == .orderedAscending } }) peers.insert(contentsOf: selectedPeers, at: 0) let state = State( peers: peers, presences: contactList.presences ) self.stateValue = state self.stateSubject.set(.single(state)) self.readySubject.set(true) }) case let .contactsSearch(query, onlyContacts): let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]), NoError> if onlyContacts { signal = combineLatest( context.engine.contacts.searchLocalPeers(query: query), context.engine.contacts.searchContacts(query: query) ) |> map { peers, contacts in let contactIds = Set(contacts.0.map { $0.id }) return (peers.filter { contactIds.contains($0.peerId) }, [:], [:]) } } else { signal = context.engine.contacts.searchLocalPeers(query: query) |> mapToSignal { peers in return context.engine.data.subscribe( EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.Presence.init)), EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) ) |> map { presenceMap, participantCountMap -> ([EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]) in return (peers, presenceMap, participantCountMap) } } } self.stateDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peers, presenceMap, participantCounts in guard let self else { return } var presences: [EnginePeer.Id: EnginePeer.Presence] = [:] for (key, value) in presenceMap { if let value { presences[key] = value } } var participants: [EnginePeer.Id: Int] = [:] for (key, value) in participantCounts { if let value { participants[key] = value } } let state = State( peers: peers.compactMap { $0.peer }.filter { peer in if case .secretChat = peer { return false } else if case let .user(user) = peer { if user.id == context.account.peerId { return false } else if user.botInfo != nil { return false } else if peer.isService { return false } else if user.isDeleted { return false } else { return true } } else if case let .channel(channel) = peer { if channel.isForum { return false } if case .broadcast = channel.info { return false } return true } else { return true } }, presences: presences, participants: participants ) self.stateValue = state self.stateSubject.set(.single(state)) self.readySubject.set(true) }) case let .members(_, peerId, searchQuery): let membersState = Promise() let contactsState = Promise() let disposableAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?) disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: searchQuery, updated: { state in membersState.set(.single(state)) }) let contactsDisposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.contacts(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: searchQuery, updated: { state in contactsState.set(.single(state)) }) let dataDisposable = combineLatest( queue: Queue.mainQueue(), contactsState.get(), membersState.get() ).startStrict(next: { [weak self] contactsState, memberState in guard let self else { return } var peers: [EnginePeer] = [] var invitedAt: [EnginePeer.Id: Int32] = [:] var existingPeersIds = Set() for participant in contactsState.list { if participant.peer.isDeleted || existingPeersIds.contains(participant.peer.id) || participant.participant.adminInfo != nil { continue } if case let .member(_, date, _, _, _) = participant.participant { invitedAt[participant.peer.id] = date } else { continue } peers.append(EnginePeer(participant.peer)) existingPeersIds.insert(participant.peer.id) } for participant in memberState.list { if participant.peer.isDeleted || existingPeersIds.contains(participant.peer.id) || participant.participant.adminInfo != nil { continue } if let user = participant.peer as? TelegramUser, user.botInfo != nil { continue } if case let .member(_, date, _, _, _) = participant.participant { invitedAt[participant.peer.id] = date } else { continue } peers.append(EnginePeer(participant.peer)) } let state = State( peers: peers, invitedAt: invitedAt ) self.stateValue = state self.stateSubject.set(.single(state)) self.readySubject.set(true) }) let combinedDisposable = DisposableSet() combinedDisposable.add(contactsDisposableAndLoadMoreControl.0) combinedDisposable.add(disposableAndLoadMoreControl.0) combinedDisposable.add(dataDisposable) self.stateDisposable = combinedDisposable self.listControl = disposableAndLoadMoreControl.1 case let .channels(_, excludePeerIds, searchQuery): self.stateDisposable = (combineLatest( context.engine.messages.chatList(group: .root, count: 500) |> take(1), searchQuery.flatMap { context.engine.contacts.searchLocalPeers(query: $0) } ?? .single([]), context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init))) ) |> mapToSignal { chatList, searchResults, initialPeers -> Signal<(EngineChatList, [EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]), NoError> in var peerIds: [EnginePeer.Id] = [] peerIds.append(contentsOf: chatList.items.map(\.renderedPeer.peerId)) peerIds.append(contentsOf: searchResults.map(\.peerId)) peerIds.append(contentsOf: initialPeers.compactMap(\.value?.id)) return context.engine.data.subscribe( EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) ) |> map { participantCountMap -> (EngineChatList, [EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]) in return (chatList, searchResults, initialPeers, participantCountMap) } } |> deliverOnMainQueue).start(next: { [weak self] chatList, searchResults, initialPeers, participantCounts in guard let self else { return } var participants: [EnginePeer.Id: Int] = [:] for (key, value) in participantCounts { if let value { participants[key] = value } } var existingIds = Set() var selectedPeers: [EnginePeer] = [] for item in chatList.items.reversed() { if let peer = item.renderedPeer.peer { if self.initialPeerIds.contains(peer.id) { selectedPeers.append(peer) existingIds.insert(peer.id) } } } for peerId in self.initialPeerIds { if !existingIds.contains(peerId), let maybePeer = initialPeers[peerId], let peer = maybePeer { selectedPeers.append(peer) existingIds.insert(peerId) } } for item in searchResults { if let peer = item.peer, case .channel = peer { selectedPeers.append(peer) existingIds.insert(peer.id) } } let queryTokens = stringIndexTokens(searchQuery ?? "", transliteration: .combined) func peerMatchesTokens(peer: EnginePeer, tokens: [ValueBoxKey]) -> Bool { if matchStringIndexTokens(peer.indexName._asIndexName().indexTokens, with: queryTokens) { return true } return false } var peers: [EnginePeer] = [] peers = chatList.items.filter { peer in if let peer = peer.renderedPeer.peer { if existingIds.contains(peer.id) { return false } if excludePeerIds.contains(peer.id) { return false } if peer.isFake || peer.isScam { return false } if let _ = searchQuery, !peerMatchesTokens(peer: peer, tokens: queryTokens) { return false } if self.initialPeerIds.contains(peer.id) { return false } if case .channel = peer { return true } return false } else { return false } }.reversed().compactMap { $0.renderedPeer.peer } for peer in peers { existingIds.insert(peer.id) } peers.insert(contentsOf: selectedPeers, at: 0) let state = State( peers: peers, participants: participants ) self.stateValue = state self.stateSubject.set(.single(state)) self.readySubject.set(true) }) } } deinit { self.stateDisposable?.dispose() } } } final class PeersListStoredState: Codable { private enum CodingKeys: String, CodingKey { case peerIds } public let peerIds: [EnginePeer.Id] public init(peerIds: [EnginePeer.Id]) { self.peerIds = peerIds } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.peerIds = try container.decode([Int64].self, forKey: .peerIds).map { EnginePeer.Id($0) } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.peerIds.map { $0.toInt64() }, forKey: .peerIds) } } private func peersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base) -> Signal<[EnginePeer.Id], NoError> { let key = EngineDataBuffer(length: 4) key.setInt32(0, value: base.rawValue) return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.shareWithPeersState, id: key)) |> map { entry -> [EnginePeer.Id] in return entry?.get(PeersListStoredState.self)?.peerIds ?? [] } } func updatePeersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base, peerIds: [EnginePeer.Id]) -> Signal { let key = EngineDataBuffer(length: 4) key.setInt32(0, value: base.rawValue) let state = PeersListStoredState(peerIds: peerIds) return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.shareWithPeersState, id: key, item: state) }