Swiftgram/submodules/TelegramUI/Sources/ChannelParticipantsScreen.swift
2020-08-03 21:04:52 +03:00

611 lines
29 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import UIKit
import SwiftSignalKit
import Display
import TelegramCore
import SyncCore
import Postbox
import TelegramPresentationData
import TelegramStringFormatting
import AccountContext
import MergeLists
import SearchBarNode
import SearchUI
import ContactListUI
import ContactsPeerItem
import ItemListUI
import ChatListSearchItemHeader
import PresentationDataUtils
class ChannelParticipantsInteraction {
let addMember: () -> Void
let openPeer: (PeerId) -> Void
init(addMember: @escaping () -> Void, openPeer: @escaping (PeerId) -> Void) {
self.addMember = addMember
self.openPeer = openPeer
}
}
private struct ChannelParticipantsTransaction {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isLoading: Bool
let isEmpty: Bool
let crossFade: Bool
}
private enum ChannelParticipantsEntryId: Hashable {
case action(Int)
case peer(PeerId)
}
private enum ChannelParticipantsEntry: Comparable, Identifiable {
case action(Int, PresentationTheme, ContactListAdditionalOption)
case peer(Int, PresentationTheme, PresentationStrings, RenderedChannelParticipant, ListViewItemHeader?, Bool)
var stableId: ChannelParticipantsEntryId {
switch self {
case let .action(index, _, _):
return .action(index)
case let .peer(_, _ , _, participant, _, _):
return .peer(participant.peer.id)
}
}
static func ==(lhs: ChannelParticipantsEntry, rhs: ChannelParticipantsEntry) -> Bool {
switch lhs {
case let .action(lhsIndex, lhsTheme, lhsOption):
if case let .action(rhsIndex, rhsTheme, rhsOption) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsOption == rhsOption {
return true
} else {
return false
}
case let .peer(lhsIndex, lhsTheme, lhsStrings, lhsParticipant, lhsHeader, lhsExpanded):
if case let .peer(rhsIndex, rhsTheme, rhsStrings, rhsParticipant, rhsHeader, rhsExpanded) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsParticipant == rhsParticipant, lhsHeader?.id == rhsHeader?.id, lhsExpanded == rhsExpanded {
return true
} else {
return false
}
}
}
static func <(lhs: ChannelParticipantsEntry, rhs: ChannelParticipantsEntry) -> Bool {
switch lhs {
case let .action(lhsIndex, _, _):
switch rhs {
case let .action(rhsIndex, _, _):
return lhsIndex < rhsIndex
case .peer:
return true
}
case let .peer(lhsIndex, _, _, _, _, _):
switch rhs {
case .action:
return false
case let .peer(rhsIndex, _, _, _, _, _):
return lhsIndex < rhsIndex
}
}
}
func item(context: AccountContext, presentationData: PresentationData, interaction: ChannelParticipantsInteraction?) -> ListViewItem {
switch self {
case let .action(_, _, option):
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: false, header: nil, action: option.action)
case let .peer(_, theme, strings, participant, header, expanded):
var status: String
if case let .member(_, invitedAt, _, _, _) = participant.participant {
status = "joined \(stringForFullDate(timestamp: invitedAt, strings: strings, dateTimeFormat: presentationData.dateTimeFormat))"
} else {
status = "owner"
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: nil), status: .custom(string: status, multiline: false), enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
interaction?.openPeer(participant.peer.id)
}, itemHighlighting: ContactItemHighlighting(), contextAction: nil)
}
}
}
private func preparedTransaction(from fromEntries: [ChannelParticipantsEntry], to toEntries: [ChannelParticipantsEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: PresentationData, interaction: ChannelParticipantsInteraction?) -> ChannelParticipantsTransaction {
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), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
return ChannelParticipantsTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, crossFade: crossFade)
}
private struct ChannelParticipantsState {
var editing: Bool
init() {
self.editing = false
}
}
private class ChannelParticipantsScreenNode: ViewControllerTracingNode {
private let context: AccountContext
private let peerId: PeerId
private var presentationData: PresentationData
private var presentationDataPromise: Promise<PresentationData>
private let membersContext: PeerInfoMembersContext
private var interaction: ChannelParticipantsInteraction?
private let listNode: ListView
var navigationBar: NavigationBar?
private var searchDisplayController: SearchDisplayController?
var requestActivateSearch: (() -> Void)?
var requestDeactivateSearch: (() -> Void)?
var requestAddMember: (() -> Void)?
var requestOpenPeer: ((PeerId) -> Void)?
var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?
var contentScrollingEnded: ((ListView) -> Bool)?
private var disposable: Disposable?
private var state: ChannelParticipantsState
private let statePromise: Promise<ChannelParticipantsState>
private var currentEntries: [ChannelParticipantsEntry] = []
private var enqueuedTransactions: [ChannelParticipantsTransaction] = []
private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)?
init(context: AccountContext, peerId: PeerId) {
self.context = context
self.peerId = peerId
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationDataPromise = Promise(presentationData)
self.membersContext = PeerInfoMembersContext(context: context, peerId: peerId)
self.state = ChannelParticipantsState()
self.statePromise = Promise(self.state)
self.listNode = ListView()
self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3)
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
super.init()
self.interaction = ChannelParticipantsInteraction(addMember: { [weak self] in
self?.requestAddMember?()
}, openPeer: { [weak self] peerId in
self?.requestOpenPeer?(peerId)
})
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.listNode)
self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), self.membersContext.state, context.account.postbox.combinedView(keys: [.basicPeer(peerId)]))
|> deliverOnMainQueue).start(next: { [weak self] presentationData, state, members, combinedView in
guard let strongSelf = self, let basicPeerView = combinedView.views[.basicPeer(peerId)] as? BasicPeerView, let enclosingPeer = basicPeerView.peer else {
return
}
strongSelf.updateState(enclosingPeer: enclosingPeer, members: members, presentationData: presentationData)
})
}
private func updateState(enclosingPeer: Peer, members: PeerInfoMembersState, presentationData: PresentationData) {
var entries: [ChannelParticipantsEntry] = []
entries.append(.action(0, presentationData.theme, ContactListAdditionalOption(title: "Add Subscribers", icon: .generic(UIImage(bundleImageName: "Contact List/AddMemberIcon")!), action: { [weak self] in
// self?.interaction?
})))
let contacts = ChatListSearchItemHeader(type: .contacts, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
let otherSubscribers = ChatListSearchItemHeader(type: .otherSubscribers, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
var known: [RenderedChannelParticipant] = []
var other: [RenderedChannelParticipant] = []
// Ilya Laktyushin 572439
// Nikolay Kudashov 552564
// Alexander Stepanov 215491
// Michael Filimonov 438078
// Peter Iakovlev 903523
// Вася Бабич 763171
// Denis Prokopov 949693
// **Dmitrybot** 230212
// Dmitry Moskovsky 659864346
// Nick Kudashov jjrjrtest 76745538
// В 102439374
// Michael Filimonov 264037907
// example 3735744
// California Kai 12549969
// Pushtest 640083077
let unknown: Set<Int64> = Set([102439374, 76745538, 264037907, 3735744, 12549969, 640083077])
for member in members.members {
if case let .channelMember(participant) = member {
if case .member = participant.participant {
if unknown.contains(participant.peer.id.toInt64()) {
other.append(participant)
} else {
known.append(participant)
}
// entries.append(.peer(entries.count, presentationData.theme, presentationData.strings, participant, nil, false))
// print(participant.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + " " + "\(participant.peer.id.toInt64())")
}
}
}
for participant in known.sorted(by: { lhs, rhs in
if case let .member(_, lhsInvitedAt, _, _, _) = lhs.participant, case let .member(_, rhsInvitedAt, _, _, _) = rhs.participant {
return lhsInvitedAt > rhsInvitedAt
} else {
return false
}
}) {
entries.append(.peer(entries.count, presentationData.theme, presentationData.strings, participant, contacts, false))
}
for participant in other.sorted(by: { lhs, rhs in
if case let .member(_, lhsInvitedAt, _, _, _) = lhs.participant, case let .member(_, rhsInvitedAt, _, _, _) = rhs.participant {
return lhsInvitedAt > rhsInvitedAt
} else {
return false
}
}) {
entries.append(.peer(entries.count, presentationData.theme, presentationData.strings, participant, otherSubscribers, false))
}
let transaction = preparedTransaction(from: self.currentEntries, to: entries, isLoading: false, isEmpty: false, crossFade: false, context: self.context, presentationData: presentationData, interaction: self.interaction)
// let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, enclosingPeer: enclosingPeer, action: { [weak self] member, action in
//// self?.action(member, action)
// })
// self.enclosingPeer = enclosingPeer
self.currentEntries = entries
self.enqueueTransaction(transaction)
}
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
guard let (containerLayout, navigationBarHeight, _) = self.containerLayout, let navigationBar = self.navigationBar else {
return
}
// self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ChatListSearchContainerNode(context: self.context, filter: self.filter, groupId: .root, openPeer: { [weak self] peer, _ in
// if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch {
// requestOpenPeerFromSearch(peer)
// }
// }, openDisabledPeer: { [weak self] peer in
// self?.requestOpenDisabledPeer?(peer)
// }, openRecentPeerOptions: { _ in
// }, openMessage: { [weak self] peer, messageId in
// if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch {
// requestOpenMessageFromSearch(peer, messageId)
// }
// }, addContact: nil, peerContextAction: nil, present: { _ in
// }), cancel: { [weak self] in
// if let requestDeactivateSearch = self?.requestDeactivateSearch {
// requestDeactivateSearch()
// }
// })
//
// self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate)
// self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
// if let strongSelf = self, let strongPlaceholderNode = placeholderNode {
// if isSearchBar {
// strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode)
// } else {
// strongSelf.insertSubnode(subnode, belowSubnode: navigationBar)
// }
// }
// }, placeholder: placeholderNode)
}
func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) {
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.deactivate(placeholder: placeholderNode)
self.searchDisplayController = nil
}
}
func scrollToTop() {
}
private func enqueueTransaction(_ transaction: ChannelParticipantsTransaction) {
self.enqueuedTransactions.append(transaction)
if let _ = self.containerLayout {
while !self.enqueuedTransactions.isEmpty {
self.dequeueTransaction()
}
}
}
private func dequeueTransaction() {
guard let layout = self.containerLayout, let transition = self.enqueuedTransactions.first else {
return
}
self.enqueuedTransactions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
if transition.crossFade {
options.insert(.AnimateCrossfade)
}
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self {
// strongSelf.activityIndicator.isHidden = !transition.isLoading
// strongSelf.emptyResultsTextNode.isHidden = transition.isLoading || !transition.isEmpty
//
// strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.Map_NoPlacesNearby, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor)
//
// strongSelf.layoutActivityIndicator(transition: .immediate)
}
})
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.containerLayout == nil
self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight)
var insets = layout.insets(options: [.input])
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
if isFirstLayout {
while !self.enqueuedTransactions.isEmpty {
self.dequeueTransaction()
}
}
}
}
public class ChannelParticipantsScreen: ViewController {
private let context: AccountContext
private let peerId: PeerId
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var controllerNode: ChannelParticipantsScreenNode {
return self.displayNode as! ChannelParticipantsScreenNode
}
let addMembersDisposable = MetaDisposable()
// private let _ready = Promise<Bool>()
// override public var ready: Promise<Bool> {
// return self._ready
// }
private var searchContentNode: NavigationBarSearchContentNode?
public init(context: AccountContext, peerId: PeerId) {
self.context = context
self.peerId = peerId
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.title = "Subscribers"
self.scrollToTop = { [weak self] in
if let strongSelf = self {
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true)
}
strongSelf.controllerNode.scrollToTop()
}
}
self.presentationDataDisposable = (self.context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
// strongSelf.updateThemeAndStrings()
}
}
})
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in
self?.activateSearch()
})
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
self.addMembersDisposable.dispose()
}
override public func loadDisplayNode() {
self.displayNode = ChannelParticipantsScreenNode(context: self.context, peerId: self.peerId)
self.controllerNode.navigationBar = self.navigationBar
self.controllerNode.requestDeactivateSearch = { [weak self] in
self?.deactivateSearch()
}
self.controllerNode.requestActivateSearch = { [weak self] in
self?.activateSearch()
}
self.controllerNode.requestAddMember = { [weak self] in
guard let strongSelf = self else {
return
}
// let disabledIds = members?.compactMap({$0.peer.id}) ?? []
let context = strongSelf.context
let peerId = strongSelf.peerId
let presentationData = strongSelf.presentationData
let contactsController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: false, searchGroups: false, searchChannels: false), options: [], filters: [.excludeSelf, .disable([])]))
contactsController.navigationPresentation = .modal
strongSelf.addMembersDisposable.set((contactsController.result
|> deliverOnMainQueue
|> castError(AddChannelMemberError.self)
|> mapToSignal { [weak contactsController] result -> Signal<Never, AddChannelMemberError> in
contactsController?.displayProgress = true
var contacts: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
contacts = peerIdsValue
}
let signal = context.peerChannelMemberCategoriesContextsManager.addMembers(account: context.account, peerId: peerId, memberIds: contacts.compactMap({ contact -> PeerId? in
switch contact {
case let .peer(contactId):
return contactId
default:
return nil
}
}))
return signal
|> ignoreValues
|> deliverOnMainQueue
|> afterCompleted {
contactsController?.dismiss()
}
}).start(error: { [weak self, weak contactsController] error in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String
switch error {
case .limitExceeded:
text = presentationData.strings.Channel_ErrorAddTooMuch
case .tooMuchJoined:
text = presentationData.strings.Invite_ChannelsTooMuch
case .generic:
text = presentationData.strings.Login_UnknownError
case .restricted:
text = presentationData.strings.Channel_ErrorAddBlocked
case .notMutualContact:
text = presentationData.strings.GroupInfo_AddUserLeftError
case let .bot(memberId):
let _ = (context.account.postbox.transaction { transaction in
return transaction.getPeer(peerId)
}
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer as? TelegramChannel else {
self?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
contactsController?.dismiss()
return
}
if peer.hasPermission(.addAdmins) {
contactsController?.displayProgress = false
self?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_AddBotErrorHaveRights, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_AddBotAsAdmin, action: {
contactsController?.dismiss()
// pushControllerImpl?(channelAdminController(context: context, peerId: peerId, adminId: memberId, initialParticipant: nil, updated: { _ in
// }, upgradedToSupergroup: { _, f in f () }, transferedOwnership: { _ in }))
})]), in: .window(.root))
} else {
self?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_AddBotErrorHaveRights, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
contactsController?.dismiss()
})
return
case .botDoesntSupportGroups:
text = presentationData.strings.Channel_BotDoesntSupportGroups
case .tooMuchBots:
text = presentationData.strings.Channel_TooMuchBots
}
self?.present(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
contactsController?.dismiss()
}))
strongSelf.push(contactsController)
}
self.controllerNode.requestOpenPeer = { [weak self] peerId in
// if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
// peerSelected(peerId)
// }
}
var isProcessingContentOffsetChanged = false
self.controllerNode.contentOffsetChanged = { [weak self] offset in
if isProcessingContentOffsetChanged {
return
}
isProcessingContentOffsetChanged = true
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateListVisibleContentOffset(offset)
isProcessingContentOffsetChanged = false
}
}
self.controllerNode.contentScrollingEnded = { [weak self] listView in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
return fixNavigationSearchableListNodeScrolling(listView, searchNode: searchContentNode)
} else {
return false
}
}
self.displayNodeDidLoad()
// self._ready.set(self.controllerNode.ready)
}
private func updatePresentationData() {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, actualNavigationBarHeight: self.navigationHeight, transition: transition)
}
private func activateSearch() {
if self.displayNavigationBar {
if let scrollToTop = self.scrollToTop {
scrollToTop()
}
if let searchContentNode = self.searchContentNode {
self.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode)
}
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
}
}
private func deactivateSearch() {
if !self.displayNavigationBar {
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
if let searchContentNode = self.searchContentNode {
self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode)
}
}
}
}