import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import TelegramCore import TelegramPresentationData import TelegramUIPreferences import ItemListUI import PresentationDataUtils import OverlayStatusController import AccountContext import AlertUI import PresentationDataUtils import AppBundle import ContextUI import TelegramStringFormatting import ItemListPeerActionItem import ItemListPeerItem import ShareController import UndoUI import QrCodeUI import PromptUI private final class FolderInviteLinkListControllerArguments { let context: AccountContext let shareMainLink: (String) -> Void let openMainLink: (String) -> Void let copyLink: (String) -> Void let mainLinkContextAction: (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void let peerAction: (EnginePeer, Bool) -> Void let toggleAllSelected: () -> Void init( context: AccountContext, shareMainLink: @escaping (String) -> Void, openMainLink: @escaping (String) -> Void, copyLink: @escaping (String) -> Void, mainLinkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void, peerAction: @escaping (EnginePeer, Bool) -> Void, toggleAllSelected: @escaping () -> Void ) { self.context = context self.shareMainLink = shareMainLink self.openMainLink = openMainLink self.copyLink = copyLink self.mainLinkContextAction = mainLinkContextAction self.peerAction = peerAction self.toggleAllSelected = toggleAllSelected } } private enum InviteLinksListSection: Int32 { case header case mainLink case peers } private enum InviteLinksListEntry: ItemListNodeEntry { enum StableId: Hashable { case index(Int) case peer(EnginePeer.Id) } case header(NSAttributedString) case mainLinkHeader(String) case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool) case peersHeader(String, String?) case peer(index: Int, peer: EnginePeer, isSelected: Bool, disabledReasonText: String?) case peersInfo(String) var section: ItemListSectionId { switch self { case .header: return InviteLinksListSection.header.rawValue case .mainLinkHeader, .mainLink: return InviteLinksListSection.mainLink.rawValue case .peersHeader, .peer, .peersInfo: return InviteLinksListSection.peers.rawValue } } var stableId: StableId { switch self { case .header: return .index(0) case .mainLinkHeader: return .index(1) case .mainLink: return .index(2) case .peersHeader: return .index(4) case .peersInfo: return .index(5) case let .peer(_, peer, _, _): return .peer(peer.id) } } var sortIndex: Int { switch self { case .header: return 0 case .mainLinkHeader: return 1 case .mainLink: return 2 case .peersHeader: return 4 case let .peer(index, _, _, _): return 10 + index case .peersInfo: return 1000 } } static func ==(lhs: InviteLinksListEntry, rhs: InviteLinksListEntry) -> Bool { switch lhs { case let .header(text): if case .header(text) = rhs { return true } else { return false } case let .mainLinkHeader(text): if case .mainLinkHeader(text) = rhs { return true } else { return false } case let .mainLink(lhsLink, lhsIsGenerating): if case let .mainLink(rhsLink, rhsIsGenerating) = rhs, lhsLink == rhsLink, lhsIsGenerating == rhsIsGenerating { return true } else { return false } case let .peersHeader(text, action): if case .peersHeader(text, action) = rhs { return true } else { return false } case let .peersInfo(text): if case .peersInfo(text) = rhs { return true } else { return false } case let .peer(index, peer, isSelected, disabledReasonText): if case .peer(index, peer, isSelected, disabledReasonText) = rhs { return true } else { return false } } } static func <(lhs: InviteLinksListEntry, rhs: InviteLinksListEntry) -> Bool { return lhs.sortIndex < rhs.sortIndex } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! FolderInviteLinkListControllerArguments switch self { case let .header(text): return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "ChatListCloudFolderLink", sectionId: self.section) case let .mainLinkHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .mainLink(link, isGenerating): return ItemListFolderInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: link, count: 0, peers: [], displayButton: true, enableButton: !isGenerating, buttonTitle: presentationData.strings.FolderLinkScreen_LinkActionCopy, secondaryButtonTitle: link != nil ? presentationData.strings.FolderLinkScreen_LinkActionShare : nil, displayImporters: false, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { if let link { arguments.copyLink(link.link) } }, shareAction: { if let link { arguments.copyLink(link.link) } }, secondaryAction: { if let link { arguments.shareMainLink(link.link) } }, contextAction: { node, gesture in arguments.mainLinkContextAction(link, node, gesture) }, viewAction: { if let link { arguments.openMainLink(link.link) } }) case let .peersHeader(text, action): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, actionText: action, action: action == nil ? nil : { arguments.toggleAllSelected() }, sectionId: self.section) case let .peersInfo(text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .peer(_, peer, isSelected, disabledReasonText): return ItemListPeerItem( presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(disabledReasonText ?? presentationData.strings.FolderLinkScreen_LabelCanInvite, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: disabledReasonText == nil), enabled: true, selectable: true, highlightable: false, sectionId: self.section, action: { arguments.peerAction(peer, disabledReasonText == nil) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in } ) } } } private func folderInviteLinkListControllerEntries( presentationData: PresentationData, state: FolderInviteLinkListControllerState, title: ChatFolderTitle, allPeers: [EnginePeer] ) -> [InviteLinksListEntry] { var entries: [InviteLinksListEntry] = [] var infoString: String? let chatCountString: NSAttributedString let peersHeaderString: String let canShareChats = !allPeers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) let allSelected = allPeers.filter({ canShareLinkToPeer(peer: $0) }).allSatisfy({ state.selectedPeerIds.contains($0.id) }) var selectAllString: String? if !canShareChats { infoString = presentationData.strings.FolderLinkScreen_TitleDescriptionUnavailable chatCountString = NSAttributedString(string: presentationData.strings.FolderLinkScreen_ChatCountHeaderUnavailable) peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeaderUnavailable } else if state.selectedPeerIds.isEmpty { let chatCountStringValue = NSMutableAttributedString(string: presentationData.strings.FolderLinkScreen_TitleDescriptionDeselectedV2) let folderRange = (chatCountStringValue.string as NSString).range(of: "{folder}") if folderRange.location != NSNotFound { chatCountStringValue.replaceCharacters(in: folderRange, with: "") chatCountStringValue.insert(title.rawAttributedString, at: folderRange.location) } chatCountString = chatCountStringValue peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeader if allPeers.count > 1 { selectAllString = allSelected ? presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionDeselectAll : presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionSelectAll } } else { let chatCountStringValue = NSMutableAttributedString(string: presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedV2) let folderRange = (chatCountStringValue.string as NSString).range(of: "{folder}") if folderRange.location != NSNotFound { chatCountStringValue.replaceCharacters(in: folderRange, with: "") chatCountStringValue.insert(title.rawAttributedString, at: folderRange.location) } let chatsRange = (chatCountStringValue.string as NSString).range(of: "{chats}") if chatsRange.location != NSNotFound { chatCountStringValue.replaceCharacters(in: chatsRange, with: "") let countValue = presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedCount(Int32(state.selectedPeerIds.count)) chatCountStringValue.insert(NSAttributedString(string: countValue), at: chatsRange.location) } chatCountString = chatCountStringValue peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeaderSelected(Int32(state.selectedPeerIds.count)) if allPeers.count > 1 { selectAllString = allSelected ? presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionDeselectAll : presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionSelectAll } } entries.append(.header(chatCountString)) if canShareChats { entries.append(.mainLinkHeader(presentationData.strings.FolderLinkScreen_LinkSectionHeader)) entries.append(.mainLink(link: state.currentLink, isGenerating: state.generatingLink)) } entries.append(.peersHeader(peersHeaderString, selectAllString)) var sortedPeers: [EnginePeer] = [] for peer in allPeers.filter({ canShareLinkToPeer(peer: $0) }) { sortedPeers.append(peer) } for peer in allPeers.filter({ !canShareLinkToPeer(peer: $0) }) { sortedPeers.append(peer) } for peer in sortedPeers { var disabledReasonText: String? if !canShareLinkToPeer(peer: peer) { if case let .user(user) = peer { if user.botInfo != nil { disabledReasonText = presentationData.strings.FolderLinkScreen_LabelUnavailableBot } else { disabledReasonText = presentationData.strings.FolderLinkScreen_LabelUnavailableUser } } else { disabledReasonText = presentationData.strings.FolderLinkScreen_LabelUnavailableGeneric } } entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), disabledReasonText: disabledReasonText)) } if let infoString { entries.append(.peersInfo(infoString)) } return entries } private struct FolderInviteLinkListControllerState: Equatable { var title: String? var currentLink: ExportedChatFolderLink? var selectedPeerIds = Set() var generatingLink: Bool = false var isSaving: Bool = false } public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filterId: Int32, title filterTitle: ChatFolderTitle, allPeerIds: [EnginePeer.Id], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void, presentController parentPresentController: ((ViewController) -> Void)?) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? let _ = pushControllerImpl var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? var attemptNavigationImpl: ((@escaping () -> Void) -> Bool)? var navigationController: (() -> NavigationController?)? var dismissTooltipsImpl: (() -> Void)? let actionsDisposable = DisposableSet() var initialState = FolderInviteLinkListControllerState() initialState.title = currentInvitation?.title initialState.currentLink = currentInvitation let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((FolderInviteLinkListControllerState) -> FolderInviteLinkListControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } let _ = updateState let revokeLinkDisposable = MetaDisposable() actionsDisposable.add(revokeLinkDisposable) let deleteAllRevokedLinksDisposable = MetaDisposable() actionsDisposable.add(deleteAllRevokedLinksDisposable) var getControllerImpl: (() -> ViewController?)? var displayTooltipImpl: ((UndoOverlayContent, Bool) -> Void)? var didDisplayAddPeerNotice: Bool = false var combinedPeerIds: [EnginePeer.Id] = [] if let currentInvitation { for peerId in currentInvitation.peerIds { if !combinedPeerIds.contains(peerId) { combinedPeerIds.append(peerId) } } } for peerId in allPeerIds { if !combinedPeerIds.contains(peerId) { combinedPeerIds.append(peerId) } } let arguments = FolderInviteLinkListControllerArguments(context: context, shareMainLink: { inviteLink in let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData) shareController.completed = { peerIds in let _ = (context.engine.data.get( EngineDataList( peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) ) ) |> deliverOnMainQueue).start(next: { peerList in let peers = peerList.compactMap { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_SavedMessages_One savedMessages = true } else { if peers.count == 1, let peer = peers.first { let peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_Chat_One(peerName).string } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { let firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) let secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string } else if let peer = peers.first { let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string } else { text = "" } } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in if savedMessages, action == .info { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer else { return } guard let navigationController = navigationController?() else { return } context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), forceOpenChat: true)) }) } return false }), nil) }) } shareController.actionCompleted = { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) } presentControllerImpl?(shareController, nil) }, openMainLink: { _ in }, copyLink: { link in UIPasteboard.general.string = link dismissTooltipsImpl?() let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) }, mainLinkContextAction: { invite, node, gesture in guard let node = node as? ContextReferenceContentNode, let controller = getControllerImpl?(), let invite = invite else { return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: presentationData.strings.FolderLinkScreen_ContextActionNameLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.dismissWithoutContent) let state = stateValue.with({ $0 }) let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: presentationData.strings.FolderLinkScreen_NameLink_Title, titleFont: .bold, value: state.title ?? "", characterLimit: 32, apply: { value in if let value { updateState { state in var state = state state.title = value return state } } }) presentControllerImpl?(promptController, nil) }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.dismissWithoutContent) dismissTooltipsImpl?() UIPasteboard.general.string = invite.link let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.dismissWithoutContent) presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .chatFolder(slug: invite.slug)), nil) }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in f(.dismissWithoutContent) let _ = (context.engine.peers.editChatFolderLink(filterId: filterId, link: invite, title: nil, peerIds: nil, revoke: true) |> deliverOnMainQueue).start(completed: { let _ = (context.engine.peers.deleteChatFolderLink(filterId: filterId, link: invite) |> deliverOnMainQueue).start(completed: { linkUpdated(nil) dismissImpl?() }) }) }))) let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, peerAction: { peer, isEnabled in let presentationData = context.sharedContext.currentPresentationData.with { $0 } if isEnabled { var added = false updateState { state in var state = state if state.selectedPeerIds.contains(peer.id) { state.selectedPeerIds.remove(peer.id) } else { state.selectedPeerIds.insert(peer.id) if let currentInvitation, !currentInvitation.peerIds.contains(peer.id) { added = true } } return state } if added && !didDisplayAddPeerNotice { didDisplayAddPeerNotice = true dismissTooltipsImpl?() displayTooltipImpl?(.info(title: nil, text: presentationData.strings.FolderLinkScreen_ToastNewChatAdded, timeout: 8, customUndoText: nil), true) } } else { let text: String if case let .user(user) = peer { if user.botInfo != nil { text = presentationData.strings.FolderLinkScreen_AlertTextUnavailableBot } else { text = presentationData.strings.FolderLinkScreen_AlertTextUnavailableUser } } else { var isGroup = true let isPrivate = peer.addressName == nil if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } if isGroup { if isPrivate { text = presentationData.strings.FolderLinkScreen_AlertTextUnavailablePrivateGroup } else { text = presentationData.strings.FolderLinkScreen_AlertTextUnavailablePublicGroup } } else { if isPrivate { text = presentationData.strings.FolderLinkScreen_AlertTextUnavailablePrivateChannel } else { text = presentationData.strings.FolderLinkScreen_AlertTextUnavailablePublicChannel } } } dismissTooltipsImpl?() displayTooltipImpl?(.peers(context: context, peers: [peer], title: nil, text: text, customUndoText: nil), true) } }, toggleAllSelected: { let _ = (context.engine.data.get( EngineDataList(combinedPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) ) |> deliverOnMainQueue).start(next: { allPeers in let allPeers = allPeers.compactMap({ $0 }) let selectablePeers = allPeers.filter({ canShareLinkToPeer(peer: $0) }) let state = stateValue.with({ $0 }) let allSelected = selectablePeers.allSatisfy({ state.selectedPeerIds.contains($0.id) }) updateState { state in var state = state if allSelected { state.selectedPeerIds.removeAll() } else { state.selectedPeerIds.removeAll() for peer in selectablePeers { state.selectedPeerIds.insert(peer.id) } } return state } }) }) let allPeers = context.engine.data.subscribe( EngineDataList(combinedPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) ) |> map { peers -> [EnginePeer] in return peers.compactMap({ peer -> EnginePeer? in guard let peer else { return nil } if case let .legacyGroup(group) = peer, group.migrationReference != nil { return nil } return peer }) } let applyChangesImpl: (() -> Void)? = { let state = stateValue.with({ $0 }) if state.selectedPeerIds.isEmpty { return } if let currentLink = state.currentLink { if currentLink.title != state.title || Set(currentLink.peerIds) != state.selectedPeerIds { updateState { state in var state = state state.isSaving = true return state } actionsDisposable.add((context.engine.peers.editChatFolderLink(filterId: filterId, link: currentLink, title: state.title, peerIds: Array(state.selectedPeerIds), revoke: false) |> deliverOnMainQueue).start(error: { _ in updateState { state in var state = state state.isSaving = false return state } dismissTooltipsImpl?() let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.FolderLinkScreen_SaveUnknownError, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) }, completed: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } linkUpdated(ExportedChatFolderLink(title: state.title ?? "", link: currentLink.link, peerIds: Array(state.selectedPeerIds), isRevoked: false)) displayTooltipImpl?(.info(title: nil, text: presentationData.strings.FolderLinkScreen_ToastLinkUpdated, timeout: 3, customUndoText: nil), false) dismissImpl?() })) } else { dismissImpl?() } } else { dismissImpl?() } } let _ = (allPeers |> take(1) |> deliverOnMainQueue).start(next: { peers in updateState { state in var state = state if let currentInvitation { for peerId in currentInvitation.peerIds { state.selectedPeerIds.insert(peerId) } } else { for peerId in peers.map(\.id) { if let peer = peers.first(where: { $0.id == peerId }) { if canShareLinkToPeer(peer: peer) { state.selectedPeerIds.insert(peerId) } } } } return state } }) let previousState = Atomic(value: nil) let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let signal = combineLatest(queue: .mainQueue(), presentationData, statePromise.get(), allPeers ) |> map { presentationData, state, allPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in let allPeers = allPeers.compactMap { $0 } let crossfade = false var animateChanges = false let previousStateValue = previousState.swap(state) if let previousStateValue, previousStateValue.selectedPeerIds != state.selectedPeerIds { animateChanges = true } let title: ItemListControllerTitle var folderTitle = presentationData.strings.FolderLinkScreen_Title if let title = state.title, !title.isEmpty { folderTitle = title } title = .text(folderTitle) var doneButton: ItemListNavigationButton? let canShareChats = !allPeers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) if !canShareChats { doneButton = nil } else if state.isSaving { doneButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { var saveEnabled = false if let currentLink = state.currentLink { if currentLink.title != state.title || Set(currentLink.peerIds) != state.selectedPeerIds { saveEnabled = true } } else { saveEnabled = true } doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Save), style: .bold, enabled: !state.selectedPeerIds.isEmpty && saveEnabled, action: { applyChangesImpl?() }) } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: doneButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: folderInviteLinkListControllerEntries( presentationData: presentationData, state: state, title: filterTitle, allPeers: allPeers ), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ItemListController(context: context, state: signal) controller.navigationPresentation = .modal controller.willDisappear = { _ in dismissTooltipsImpl?() } controller.didDisappear = { [weak controller] _ in controller?.clearItemNodesHighlight(animated: true) } controller.visibleBottomContentOffsetChanged = { offset in if case let .known(value) = offset, value < 40.0 { } } controller.attemptNavigation = { f in return attemptNavigationImpl?(f) ?? true } attemptNavigationImpl = { f in if let currentInvitation { let state = stateValue.with({ $0 }) var hasChanges = false if state.title != currentInvitation.title { hasChanges = true } if state.selectedPeerIds != Set(currentInvitation.peerIds) { hasChanges = true } if hasChanges { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.FolderLinkScreen_SaveAlertTitle, text: presentationData.strings.FolderLinkScreen_SaveAlertText, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.FolderLinkScreen_SaveAlertActionDiscard, action: { f() dismissImpl?() }), TextAlertAction(type: .defaultAction, title: state.selectedPeerIds.isEmpty ? presentationData.strings.FolderLinkScreen_SaveAlertActionApply : presentationData.strings.FolderLinkScreen_SaveAlertActionContinue, action: { applyChangesImpl?() }) ]), nil) return false } else { f() return true } } else { f() return true } } navigationController = { [weak controller] in return controller?.navigationController as? NavigationController } pushControllerImpl = { [weak controller] c in if let controller = controller { (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) } } presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) } } presentInGlobalOverlayImpl = { [weak controller] c in if let controller = controller { controller.presentInGlobalOverlay(c) } } dismissImpl = { [weak controller] in controller?.dismiss() } getControllerImpl = { [weak controller] in return controller } displayTooltipImpl = { [weak controller] c, inCurrentContext in let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) if let controller = controller, inCurrentContext { controller.present(UndoOverlayController(presentationData: presentationData, content: c, elevatedLayout: false, action: { _ in return false }), in: .current) } else if !inCurrentContext { parentPresentController?(UndoOverlayController(presentationData: presentationData, content: c, elevatedLayout: false, action: { _ in return false })) } } dismissTooltipsImpl = { [weak controller] in controller?.forEachController({ controller in if let controller = controller as? UndoOverlayController { controller.dismissWithCommitAction() } return true }) } return controller }