import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences import ItemListUI import PresentationDataUtils import OverlayStatusController import AccountContext import ShareController import AlertUI import PresentationDataUtils import TelegramNotices import ItemListPeerItem import ItemListPeerActionItem import AccountContext import InviteLinksUI import ContextUI import UndoUI import QrCodeUI import PremiumUI import TextFormat import PremiumUI import OldChannelsController private final class ChannelVisibilityControllerArguments { let context: AccountContext let updateCurrentType: (CurrentChannelType) -> Void let updatePublicLinkText: (String?, String) -> Void let scrollToPublicLinkText: () -> Void let setPeerIdWithRevealedOptions: (EnginePeer.Id?, EnginePeer.Id?) -> Void let revokePeerId: (EnginePeer.Id) -> Void let copyLink: (ExportedInvitation) -> Void let shareLink: (ExportedInvitation) -> Void let linkContextAction: (ASDisplayNode, ContextGesture?) -> Void let manageInviteLinks: () -> Void let openLink: (ExportedInvitation) -> Void let toggleForwarding: (Bool) -> Void let updateJoinToSend: (CurrentChannelJoinToSend) -> Void let toggleApproveMembers: (Bool) -> Void let activateLink: (String) -> Void let deactivateLink: (String) -> Void let openAuction: (String) -> Void init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, revokePeerId: @escaping (EnginePeer.Id) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, linkContextAction: @escaping (ASDisplayNode, ContextGesture?) -> Void, manageInviteLinks: @escaping () -> Void, openLink: @escaping (ExportedInvitation) -> Void, toggleForwarding: @escaping (Bool) -> Void, updateJoinToSend: @escaping (CurrentChannelJoinToSend) -> Void, toggleApproveMembers: @escaping (Bool) -> Void, activateLink: @escaping (String) -> Void, deactivateLink: @escaping (String) -> Void, openAuction: @escaping (String) -> Void) { self.context = context self.updateCurrentType = updateCurrentType self.updatePublicLinkText = updatePublicLinkText self.scrollToPublicLinkText = scrollToPublicLinkText self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.revokePeerId = revokePeerId self.copyLink = copyLink self.shareLink = shareLink self.linkContextAction = linkContextAction self.manageInviteLinks = manageInviteLinks self.openLink = openLink self.toggleForwarding = toggleForwarding self.updateJoinToSend = updateJoinToSend self.toggleApproveMembers = toggleApproveMembers self.activateLink = activateLink self.deactivateLink = deactivateLink self.openAuction = openAuction } } private enum ChannelVisibilitySection: Int32 { case type case limitInfo case link case linkActions case additional case joinToSend case approveMembers case forwarding } private enum ChannelVisibilityEntryTag: ItemListItemTag { case publicLink case privateLink func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? ChannelVisibilityEntryTag, self == other { return true } else { return false } } } private enum ChannelVisibilityEntryId: Hashable { case index(Int32) case username(String) } private enum ChannelVisibilityEntry: ItemListNodeEntry { case typeHeader(PresentationTheme, String) case typePublic(PresentationTheme, String, Bool) case typePrivate(PresentationTheme, String, Bool) case typeInfo(PresentationTheme, String) case publicLinkHeader(PresentationTheme, String) case publicLinkAvailability(PresentationTheme, String, Bool) case linksLimitInfo(PresentationTheme, String, Int32, Int32, Int32, Bool) case editablePublicLink(PresentationTheme, PresentationStrings, String, String) case privateLinkHeader(PresentationTheme, String) case privateLink(PresentationTheme, ExportedInvitation?, [EnginePeer], Int32, Bool) case privateLinkInfo(PresentationTheme, String) case privateLinkManage(PresentationTheme, String) case privateLinkManageInfo(PresentationTheme, String) case publicLinkInfo(PresentationTheme, String) case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus, String) case existingLinksInfo(PresentationTheme, String) case existingLinkPeerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, EnginePeer, ItemListPeerItemEditing, Bool) case additionalLinkHeader(PresentationTheme, String) case additionalLink(PresentationTheme, TelegramPeerUsername, Int32) case additionalLinkInfo(PresentationTheme, String) case joinToSendHeader(PresentationTheme, String) case joinToSendEveryone(PresentationTheme, String, Bool) case joinToSendMembers(PresentationTheme, String, Bool) case approveMembers(PresentationTheme, String, Bool) case approveMembersInfo(PresentationTheme, String) case forwardingHeader(PresentationTheme, String) case forwardingDisabled(PresentationTheme, String, Bool) case forwardingInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { case .typeHeader, .typePublic, .typePrivate, .typeInfo: return ChannelVisibilitySection.type.rawValue case .linksLimitInfo: return ChannelVisibilitySection.limitInfo.rawValue case .publicLinkHeader, .publicLinkAvailability, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus: return ChannelVisibilitySection.link.rawValue case .additionalLinkHeader, .additionalLink, .additionalLinkInfo: return ChannelVisibilitySection.additional.rawValue case .privateLinkManage, .privateLinkManageInfo: return ChannelVisibilitySection.linkActions.rawValue case .existingLinksInfo, .existingLinkPeerItem: return ChannelVisibilitySection.link.rawValue case .joinToSendHeader, .joinToSendEveryone, .joinToSendMembers: return ChannelVisibilitySection.joinToSend.rawValue case .approveMembers, .approveMembersInfo: return ChannelVisibilitySection.approveMembers.rawValue case .forwardingHeader, .forwardingDisabled, .forwardingInfo: return ChannelVisibilitySection.forwarding.rawValue } } var stableId: ChannelVisibilityEntryId { switch self { case .typeHeader: return .index(0) case .typePublic: return .index(1) case .typePrivate: return .index(2) case .typeInfo: return .index(3) case .publicLinkHeader: return .index(4) case .publicLinkAvailability: return .index(5) case .linksLimitInfo: return .index(6) case .privateLinkHeader: return .index(7) case .privateLink: return .index(8) case .editablePublicLink: return .index(9) case .privateLinkInfo: return .index(10) case .publicLinkStatus: return .index(11) case .publicLinkInfo: return .index(12) case .existingLinksInfo: return .index(13) case let .existingLinkPeerItem(index, _, _, _, _, _, _, _): return .index(14 + index) case .additionalLinkHeader: return .index(1000) case let .additionalLink(_, username, _): return .username(username.username) case .additionalLinkInfo: return .index(2000) case .privateLinkManage: return .index(2001) case .privateLinkManageInfo: return .index(2002) case .joinToSendHeader: return .index(2003) case .joinToSendEveryone: return .index(2004) case .joinToSendMembers: return .index(2005) case .approveMembers: return .index(2006) case .approveMembersInfo: return .index(2007) case .forwardingHeader: return .index(2008) case .forwardingDisabled: return .index(2009) case .forwardingInfo: return .index(2010) } } static func ==(lhs: ChannelVisibilityEntry, rhs: ChannelVisibilityEntry) -> Bool { switch lhs { case let .typeHeader(lhsTheme, lhsTitle): if case let .typeHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { return true } else { return false } case let .typePublic(lhsTheme, lhsTitle, lhsSelected): if case let .typePublic(rhsTheme, rhsTitle, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSelected == rhsSelected { return true } else { return false } case let .typePrivate(lhsTheme, lhsTitle, lhsSelected): if case let .typePrivate(rhsTheme, rhsTitle, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSelected == rhsSelected { return true } else { return false } case let .typeInfo(lhsTheme, lhsText): if case let .typeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .publicLinkHeader(lhsTheme, lhsTitle): if case let .publicLinkHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { return true } else { return false } case let .publicLinkAvailability(lhsTheme, lhsText, lhsValue): if case let .publicLinkAvailability(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .linksLimitInfo(lhsTheme, lhsText, lhsCount, lhsLimit, lhsPremiumLimit, lhsIsPremiumDisabled): if case let .linksLimitInfo(rhsTheme, rhsText, rhsCount, rhsLimit, rhsPremiumLimit, rhsIsPremiumDisabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsCount == rhsCount, lhsLimit == rhsLimit, lhsPremiumLimit == rhsPremiumLimit, lhsIsPremiumDisabled == rhsIsPremiumDisabled { return true } else { return false } case let .additionalLinkHeader(lhsTheme, lhsText): if case let .additionalLinkHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .additionalLink(lhsTheme, lhsAddressName, lhsIndex): if case let .additionalLink(rhsTheme, rhsAddressName, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsIndex == rhsIndex { return true } else { return false } case let .additionalLinkInfo(lhsTheme, lhsText): if case let .additionalLinkInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .privateLinkHeader(lhsTheme, lhsTitle): if case let .privateLinkHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { return true } else { return false } case let .privateLink(lhsTheme, lhsInvite, lhsPeers, lhsImportersCount, lhsDisplayImporters): if case let .privateLink(rhsTheme, rhsInvite, rhsPeers, rhsImportersCount, rhsDisplayImporters) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite, lhsPeers == rhsPeers, lhsImportersCount == rhsImportersCount, lhsDisplayImporters == rhsDisplayImporters { return true } else { return false } case let .editablePublicLink(lhsTheme, lhsStrings, lhsPlaceholder, lhsCurrentText): if case let .editablePublicLink(rhsTheme, rhsStrings, rhsPlaceholder, rhsCurrentText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsCurrentText == rhsCurrentText { return true } else { return false } case let .privateLinkInfo(lhsTheme, lhsText): if case let .privateLinkInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .privateLinkManage(lhsTheme, lhsText): if case let .privateLinkManage(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .privateLinkManageInfo(lhsTheme, lhsText): if case let .privateLinkManageInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .publicLinkInfo(lhsTheme, lhsText): if case let .publicLinkInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .publicLinkStatus(lhsTheme, lhsText, lhsStatus, lhsUsername): if case let .publicLinkStatus(rhsTheme, rhsText, rhsStatus, rhsUsername) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStatus == rhsStatus, lhsUsername == rhsUsername { return true } else { return false } case let .existingLinksInfo(lhsTheme, lhsText): if case let .existingLinksInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .existingLinkPeerItem(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsPeer, lhsEditing, lhsEnabled): if case let .existingLinkPeerItem(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsPeer, rhsEditing, rhsEnabled) = rhs { if lhsIndex != rhsIndex { return false } if lhsTheme !== rhsTheme { return false } if lhsStrings !== rhsStrings { return false } if lhsDateTimeFormat != rhsDateTimeFormat { return false } if lhsNameOrder != rhsNameOrder { return false } if lhsPeer != rhsPeer { return false } if lhsEditing != rhsEditing { return false } if lhsEnabled != rhsEnabled { return false } return true } else { return false } case let .joinToSendHeader(lhsTheme, lhsText): if case let .joinToSendHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .joinToSendEveryone(lhsTheme, lhsText, lhsValue): if case let .joinToSendEveryone(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .joinToSendMembers(lhsTheme, lhsText, lhsValue): if case let .joinToSendMembers(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .approveMembers(lhsTheme, lhsText, lhsValue): if case let .approveMembers(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .approveMembersInfo(lhsTheme, lhsText): if case let .approveMembersInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .forwardingHeader(lhsTheme, lhsText): if case let .forwardingHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .forwardingDisabled(lhsTheme, lhsText, lhsValue): if case let .forwardingDisabled(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .forwardingInfo(lhsTheme, lhsText): if case let .forwardingInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } } } static func <(lhs: ChannelVisibilityEntry, rhs: ChannelVisibilityEntry) -> Bool { switch lhs { case .typeHeader: switch rhs { case .typeHeader: return false default: return true } case .typePublic: switch rhs { case .typeHeader, .typePublic: return false default: return true } case .typePrivate: switch rhs { case .typeHeader, .typePublic, .typePrivate: return false default: return true } case .typeInfo: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo: return false default: return true } case .publicLinkHeader: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader: return false default: return true } case .publicLinkAvailability: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability: return false default: return true } case .linksLimitInfo: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo: return false default: return true } case .privateLinkHeader: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader: return false default: return true } case .privateLink: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink: return false default: return true } case .editablePublicLink: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink: return false default: return true } case .privateLinkInfo: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo: return false default: return true } case .publicLinkStatus: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus: return false default: return true } case .publicLinkInfo: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo: return false default: return true } case .existingLinksInfo: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo: return false default: return true } case let .existingLinkPeerItem(lhsIndex, _, _, _, _, _, _, _): switch rhs { case let .existingLinkPeerItem(rhsIndex, _, _, _, _, _, _, _): return lhsIndex < rhsIndex case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo: return false default: return true } case .additionalLinkHeader: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader: return false default: return true } case let .additionalLink(_, _, lhsIndex): switch rhs { case let .additionalLink(_, _, rhsIndex): return lhsIndex < rhsIndex case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader: return false default: return true } case .additionalLinkInfo: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader, .additionalLink, .additionalLinkInfo: return false default: return true } case .privateLinkManage: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader, .additionalLink, .additionalLinkInfo, .privateLinkManage: return false default: return true } case .privateLinkManageInfo: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader, .additionalLink, .additionalLinkInfo, .privateLinkManage, .privateLinkManageInfo: return false default: return true } case .joinToSendHeader: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader, .additionalLink, .additionalLinkInfo, .privateLinkManage, .privateLinkManageInfo, .joinToSendHeader: return false default: return true } case .joinToSendEveryone: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader, .additionalLink, .additionalLinkInfo, .privateLinkManage, .privateLinkManageInfo, .joinToSendHeader, .joinToSendEveryone: return false default: return true } case .joinToSendMembers: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader, .additionalLink, .additionalLinkInfo, .privateLinkManage, .privateLinkManageInfo, .joinToSendHeader, .joinToSendEveryone, .joinToSendMembers: return false default: return true } case .approveMembers: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader, .additionalLink, .additionalLinkInfo, .privateLinkManage, .privateLinkManageInfo, .joinToSendHeader, .joinToSendEveryone, .joinToSendMembers, .approveMembers: return false default: return true } case .approveMembersInfo: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader, .additionalLink, .additionalLinkInfo, .privateLinkManage, .privateLinkManageInfo, .joinToSendHeader, .joinToSendEveryone, .joinToSendMembers, .approveMembers, .approveMembersInfo: return false default: return true } case .forwardingHeader: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader, .additionalLink, .additionalLinkInfo, .privateLinkManage, .privateLinkManageInfo, .joinToSendHeader, .joinToSendEveryone, .joinToSendMembers, .approveMembers, .approveMembersInfo, .forwardingHeader: return false default: return true } case .forwardingDisabled: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader, .additionalLink, .additionalLinkInfo, .privateLinkManage, .privateLinkManageInfo, .joinToSendHeader, .joinToSendEveryone, .joinToSendMembers, .approveMembers, .approveMembersInfo, .forwardingHeader, .forwardingDisabled: return false default: return true } case .forwardingInfo: switch rhs { case .typeHeader, .typePublic, .typePrivate, .typeInfo, .publicLinkHeader, .publicLinkAvailability, .linksLimitInfo, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkStatus, .publicLinkInfo, .existingLinksInfo, .existingLinkPeerItem, .additionalLinkHeader, .additionalLink, .additionalLinkInfo, .privateLinkManage, .privateLinkManageInfo, .joinToSendHeader, .joinToSendEveryone, .joinToSendMembers, .approveMembers, .approveMembersInfo, .forwardingHeader, .forwardingDisabled, .forwardingInfo: return false } } } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelVisibilityControllerArguments switch self { case let .typeHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .typePublic(_, text, selected): return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateCurrentType(.publicChannel) }) case let .typePrivate(_, text, selected): return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateCurrentType(.privateChannel) }) case let .typeInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .publicLinkHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .publicLinkAvailability(_, text, value): return ItemListActivityTextItem(displayActivity: value, presentationData: presentationData, text: text, color: value ? .generic : .destructive, sectionId: self.section) case let .linksLimitInfo(theme, text, count, limit, premiumLimit, isPremiumDisabled): return IncreaseLimitHeaderItem(theme: theme, strings: presentationData.strings, icon: .link, count: count, limit: limit, premiumCount: premiumLimit, text: text, isPremiumDisabled: isPremiumDisabled, sectionId: self.section) case let .privateLinkHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .privateLink(_, invite, peers, importersCount, displayImporters): return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: importersCount, peers: peers, displayButton: true, displayImporters: displayImporters, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { if let invite = invite { arguments.copyLink(invite) } }, shareAction: { if let invite = invite { arguments.shareLink(invite) } }, contextAction: { node, gesture in arguments.linkContextAction(node, gesture) }, viewAction: { if let invite = invite { arguments.openLink(invite) } }, openCallAction: { }) case let .editablePublicLink(theme, _, placeholder, currentText): return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: currentText, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), clearType: .always, tag: ChannelVisibilityEntryTag.publicLink, sectionId: self.section, textUpdated: { updatedText in arguments.updatePublicLinkText(currentText, updatedText) }, updatedFocus: { focus in if focus { arguments.scrollToPublicLinkText() } }, action: { }) case let .privateLinkInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .privateLinkManage(theme, text): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(theme), title: text, sectionId: self.section, editing: false, action: { arguments.manageInviteLinks() }) case let .privateLinkManageInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .publicLinkInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .publicLinkStatus(_, text, status, username): var displayActivity = false let textColor: ItemListActivityTextItem.TextColor switch status { case .invalidFormat: textColor = .destructive case let .availability(availability): switch availability { case .available: textColor = .constructive case .invalid: textColor = .destructive case .taken: textColor = .destructive case .purchaseAvailable: textColor = .generic } case .checking: textColor = .generic displayActivity = true } return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: text, color: textColor, linkAction: { _ in arguments.openAuction(username) }, sectionId: self.section) case let .existingLinksInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .existingLinkPeerItem(_, _, _, dateTimeFormat, nameDisplayOrder, peer, editing, enabled): var label = "" if let addressName = peer.addressName { label = "t.me/" + addressName } return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(label, .secondary), label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.revokePeerId(peerId) }) case let .additionalLinkHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .additionalLink(_, link, _): return AdditionalLinkItem(presentationData: presentationData, username: link, sectionId: self.section, style: .blocks, tapAction: { if !link.flags.contains(.isEditable) { if link.flags.contains(.isActive) { arguments.deactivateLink(link.username) } else { arguments.activateLink(link.username) } } }) case let .additionalLinkInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .joinToSendHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .joinToSendEveryone(_, text, selected): return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateJoinToSend(.everyone) arguments.toggleApproveMembers(false) }) case let .joinToSendMembers(_, text, selected): return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateJoinToSend(.members) }) case let .approveMembers(_, text, selected): return ItemListSwitchItem(presentationData: presentationData, title: text, value: selected, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleApproveMembers(value) }) case let .approveMembersInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .forwardingHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .forwardingDisabled(_, text, selected): return ItemListSwitchItem(presentationData: presentationData, title: text, value: selected, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleForwarding(!value) }) case let .forwardingInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } private enum CurrentChannelType { case publicChannel case privateChannel } private enum CurrentChannelLocation: Equatable { case removed case location(PeerGeoLocation) } private enum CurrentChannelJoinToSend { case everyone case members } private struct ChannelVisibilityControllerState: Equatable { let selectedType: CurrentChannelType? let editingPublicLinkText: String? let addressNameValidationStatus: AddressNameValidationStatus? let updatingAddressName: Bool let revealedRevokePeerId: EnginePeer.Id? let revokingPeerId: EnginePeer.Id? let revokingPrivateLink: Bool let forwardingEnabled: Bool? let joinToSend: CurrentChannelJoinToSend? let approveMembers: Bool? init() { self.selectedType = nil self.editingPublicLinkText = nil self.addressNameValidationStatus = nil self.updatingAddressName = false self.revealedRevokePeerId = nil self.revokingPeerId = nil self.revokingPrivateLink = false self.forwardingEnabled = nil self.joinToSend = nil self.approveMembers = nil } init(selectedType: CurrentChannelType?, editingPublicLinkText: String?, addressNameValidationStatus: AddressNameValidationStatus?, updatingAddressName: Bool, revealedRevokePeerId: PeerId?, revokingPeerId: PeerId?, revokingPrivateLink: Bool, forwardingEnabled: Bool?, joinToSend: CurrentChannelJoinToSend?, approveMembers: Bool?) { self.selectedType = selectedType self.editingPublicLinkText = editingPublicLinkText self.addressNameValidationStatus = addressNameValidationStatus self.updatingAddressName = updatingAddressName self.revealedRevokePeerId = revealedRevokePeerId self.revokingPeerId = revokingPeerId self.revokingPrivateLink = revokingPrivateLink self.forwardingEnabled = forwardingEnabled self.joinToSend = joinToSend self.approveMembers = approveMembers } static func ==(lhs: ChannelVisibilityControllerState, rhs: ChannelVisibilityControllerState) -> Bool { if lhs.selectedType != rhs.selectedType { return false } if lhs.editingPublicLinkText != rhs.editingPublicLinkText { return false } if lhs.addressNameValidationStatus != rhs.addressNameValidationStatus { return false } if lhs.updatingAddressName != rhs.updatingAddressName { return false } if lhs.revealedRevokePeerId != rhs.revealedRevokePeerId { return false } if lhs.revokingPeerId != rhs.revokingPeerId { return false } if lhs.revokingPrivateLink != rhs.revokingPrivateLink { return false } if lhs.forwardingEnabled != rhs.forwardingEnabled { return false } if lhs.joinToSend != rhs.joinToSend { return false } if lhs.approveMembers != rhs.approveMembers { return false } return true } func withUpdatedSelectedType(_ selectedType: CurrentChannelType?) -> ChannelVisibilityControllerState { return ChannelVisibilityControllerState(selectedType: selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) } func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?) -> ChannelVisibilityControllerState { return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) } func withUpdatedAddressNameValidationStatus(_ addressNameValidationStatus: AddressNameValidationStatus?) -> ChannelVisibilityControllerState { return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) } func withUpdatedUpdatingAddressName(_ updatingAddressName: Bool) -> ChannelVisibilityControllerState { return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) } func withUpdatedRevealedRevokePeerId(_ revealedRevokePeerId: EnginePeer.Id?) -> ChannelVisibilityControllerState { return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) } func withUpdatedRevokingPeerId(_ revokingPeerId: EnginePeer.Id?) -> ChannelVisibilityControllerState { return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) } func withUpdatedRevokingPrivateLink(_ revokingPrivateLink: Bool) -> ChannelVisibilityControllerState { return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) } func withUpdatedForwardingEnabled(_ forwardingEnabled: Bool) -> ChannelVisibilityControllerState { return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) } func withUpdatedJoinToSend(_ joinToSend: CurrentChannelJoinToSend?) -> ChannelVisibilityControllerState { return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: joinToSend, approveMembers: self.approveMembers) } func withUpdatedApproveMembers(_ approveMembers: Bool) -> ChannelVisibilityControllerState { return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: approveMembers) } } private func channelVisibilityControllerEntries(presentationData: PresentationData, mode: ChannelVisibilityControllerMode, view: PeerView, publicChannelsToRevoke: [EnginePeer]?, importers: PeerInvitationImportersState?, state: ChannelVisibilityControllerState, limits: EngineConfiguration.UserLimits, premiumLimits: EngineConfiguration.UserLimits, isPremium: Bool, isPremiumDisabled: Bool, temporaryOrder: [String]?) -> [ChannelVisibilityEntry] { var entries: [ChannelVisibilityEntry] = [] let isInitialSetup: Bool if case .initialSetup = mode { isInitialSetup = true } else { isInitialSetup = false } if let peer = view.peers[view.peerId] as? TelegramChannel { var isGroup = false if case .group = peer.info { isGroup = true } let selectedType: CurrentChannelType if case .privateLink = mode { selectedType = .privateChannel } else { if let current = state.selectedType { selectedType = current } else { if let addressName = peer.addressName, !addressName.isEmpty { selectedType = .publicChannel } else if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil { selectedType = .publicChannel } else if case .initialSetup = mode { selectedType = .privateChannel } else { selectedType = .privateChannel } } } let joinToSend: CurrentChannelJoinToSend if let current = state.joinToSend { joinToSend = current } else { if peer.flags.contains(.joinToSend) { joinToSend = .members } else { joinToSend = .everyone } } let approveMembers: Bool if let enabled = state.approveMembers { approveMembers = enabled } else { if peer.flags.contains(.requestToJoin) { approveMembers = true } else { approveMembers = false } } let forwardingEnabled: Bool if let enabled = state.forwardingEnabled { forwardingEnabled = enabled } else { if peer.flags.contains(.copyProtectionEnabled) { forwardingEnabled = false } else { forwardingEnabled = true } } let currentUsername: String if let current = state.editingPublicLinkText { currentUsername = current } else { if let username = peer.editableUsername { currentUsername = username } else { currentUsername = "" } } if let _ = (view.cachedData as? CachedChannelData)?.peerGeoLocation { } else { switch mode { case .privateLink, .revokeNames: break case .initialSetup, .generic: entries.append(.typeHeader(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_TypeHeader.uppercased() : presentationData.strings.Channel_Edit_LinkItem.uppercased())) entries.append(.typePublic(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_TypePublic : presentationData.strings.Channel_Setup_LinkTypePublic, selectedType == .publicChannel)) entries.append(.typePrivate(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_TypePrivate : presentationData.strings.Channel_Setup_LinkTypePrivate, selectedType == .privateChannel)) switch selectedType { case .publicChannel: if isGroup { entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePublicHelp)) } else { entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePublicHelp)) } case .privateChannel: if isGroup { entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePrivateHelp)) } else { entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivateHelp)) } } } } let otherUsernames = peer.usernames.filter { !$0.flags.contains(.isEditable) } if case .revokeNames = mode { let count = Int32(publicChannelsToRevoke?.count ?? 0) let text: String if count >= premiumLimits.maxPublicLinksCount { text = presentationData.strings.Group_Username_RemoveExistingUsernamesFinalInfo } else { if isPremiumDisabled { text = presentationData.strings.Group_Username_RemoveExistingUsernamesNoPremiumInfo } else { text = presentationData.strings.Group_Username_RemoveExistingUsernamesOrExtendInfo("\(premiumLimits.maxPublicLinksCount)").string } } entries.append(.linksLimitInfo(presentationData.theme, text, count, limits.maxPublicLinksCount, premiumLimits.maxPublicLinksCount, isPremiumDisabled)) if let publicChannelsToRevoke = publicChannelsToRevoke { var index: Int32 = 0 for peer in publicChannelsToRevoke.sorted(by: { lhs, rhs in var lhsDate: Int32 = 0 var rhsDate: Int32 = 0 if case let .channel(lhs) = lhs { lhsDate = lhs.creationDate } if case let .channel(rhs) = rhs { rhsDate = rhs.creationDate } return lhsDate > rhsDate }) { entries.append(.existingLinkPeerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peer, ItemListPeerItemEditing(editable: true, editing: true, revealed: state.revealedRevokePeerId == peer.id), state.revokingPeerId == nil)) index += 1 } } } else { switch selectedType { case .publicChannel: entries.append(.editablePublicLink(presentationData.theme, presentationData.strings, presentationData.strings.Group_PublicLink_Placeholder, currentUsername)) if let status = state.addressNameValidationStatus { let text: String switch status { case let .invalidFormat(error): switch error { case .startsWithDigit: if isGroup { text = presentationData.strings.Group_Username_InvalidStartsWithNumber } else { text = presentationData.strings.Channel_Username_InvalidStartsWithNumber } case .startsWithUnderscore: if isGroup { text = presentationData.strings.Group_Username_InvalidStartsWithUnderscore } else { text = presentationData.strings.Channel_Username_InvalidStartsWithUnderscore } case .endsWithUnderscore: if isGroup { text = presentationData.strings.Group_Username_InvalidEndsWithUnderscore } else { text = presentationData.strings.Channel_Username_InvalidEndsWithUnderscore } case .tooShort: if isGroup { text = presentationData.strings.Group_Username_InvalidTooShort } else { text = presentationData.strings.Channel_Username_InvalidTooShort } case .invalidCharacters: text = presentationData.strings.Channel_Username_InvalidCharacters } case let .availability(availability): switch availability { case .available: text = presentationData.strings.Channel_Username_UsernameIsAvailable(currentUsername).string case .invalid: text = presentationData.strings.Channel_Username_InvalidCharacters case .taken: text = presentationData.strings.Channel_Username_InvalidTaken case .purchaseAvailable: var markdownString = presentationData.strings.Channel_Username_UsernamePurchaseAvailable let entities = generateTextEntities(markdownString, enabledTypes: [.mention]) if let entity = entities.first { markdownString.insert(contentsOf: "]()", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.upperBound)) markdownString.insert(contentsOf: "[", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.lowerBound)) } text = markdownString } case .checking: text = presentationData.strings.Channel_Username_CheckingUsername } entries.append(.publicLinkStatus(presentationData.theme, text, status, currentUsername)) } if isGroup { if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil { entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_PublicLink_Info)) } else { entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp)) } } else { entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Channel_Username_CreatePublicLinkHelp)) } if !otherUsernames.isEmpty { entries.append(.additionalLinkHeader(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_LinksOrder : presentationData.strings.Channel_Setup_LinksOrder)) var usernames = peer.usernames if let temporaryOrder = temporaryOrder { var usernamesMap: [String: TelegramPeerUsername] = [:] for username in usernames { usernamesMap[username.username] = username } var sortedUsernames: [TelegramPeerUsername] = [] for username in temporaryOrder { if let username = usernamesMap[username] { sortedUsernames.append(username) } } usernames = sortedUsernames } var i: Int32 = 0 for username in usernames { entries.append(.additionalLink(presentationData.theme, username, i)) i += 1 } entries.append(.additionalLinkInfo(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_LinksOrderInfo : presentationData.strings.Channel_Setup_LinksOrderInfo)) } switch mode { case .initialSetup, .revokeNames: break case .generic, .privateLink: entries.append(.privateLinkManage(presentationData.theme, presentationData.strings.InviteLink_Manage)) entries.append(.privateLinkManageInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo)) } case .privateChannel: let invite = (view.cachedData as? CachedChannelData)?.exportedInvitation entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_InviteLink.uppercased())) entries.append(.privateLink(presentationData.theme, invite, importers?.importers.prefix(3).compactMap { $0.peer.peer.flatMap(EnginePeer.init) } ?? [], importers?.count ?? 0, !isInitialSetup)) if isGroup { entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp)) } else { entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Channel_Username_CreatePrivateLinkHelp)) } switch mode { case .initialSetup, .revokeNames: break case .generic, .privateLink: entries.append(.privateLinkManage(presentationData.theme, presentationData.strings.InviteLink_Manage)) entries.append(.privateLinkManageInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo)) } } var isDiscussion = false if let cachedData = view.cachedData as? CachedChannelData, case let .known(peerId) = cachedData.linkedDiscussionPeerId, peerId != nil { isDiscussion = true } if isGroup && (selectedType == .publicChannel || isDiscussion) { if isDiscussion { entries.append(.joinToSendHeader(presentationData.theme, presentationData.strings.Group_Setup_WhoCanSendMessages_Title.uppercased())) entries.append(.joinToSendEveryone(presentationData.theme, presentationData.strings.Group_Setup_WhoCanSendMessages_Everyone, joinToSend == .everyone)) entries.append(.joinToSendMembers(presentationData.theme, presentationData.strings.Group_Setup_WhoCanSendMessages_OnlyMembers, joinToSend == .members)) } if !isDiscussion || joinToSend == .members { entries.append(.approveMembers(presentationData.theme, presentationData.strings.Group_Setup_ApproveNewMembers, approveMembers)) entries.append(.approveMembersInfo(presentationData.theme, presentationData.strings.Group_Setup_ApproveNewMembersInfo)) } } entries.append(.forwardingHeader(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_ForwardingGroupTitle.uppercased() : presentationData.strings.Group_Setup_ForwardingChannelTitle.uppercased())) entries.append(.forwardingDisabled(presentationData.theme, presentationData.strings.Group_Setup_ForwardingDisabled, !forwardingEnabled)) entries.append(.forwardingInfo(presentationData.theme, forwardingEnabled ? (isGroup ? presentationData.strings.Group_Setup_ForwardingGroupInfo : presentationData.strings.Group_Setup_ForwardingChannelInfo) : (isGroup ? presentationData.strings.Group_Setup_ForwardingGroupInfoDisabled : presentationData.strings.Group_Setup_ForwardingChannelInfoDisabled))) } } else if let peer = view.peers[view.peerId] as? TelegramGroup { if case .revokeNames = mode { let count = Int32(publicChannelsToRevoke?.count ?? 0) let text: String if count >= premiumLimits.maxPublicLinksCount { text = presentationData.strings.Group_Username_RemoveExistingUsernamesFinalInfo } else { if isPremiumDisabled { text = presentationData.strings.Group_Username_RemoveExistingUsernamesNoPremiumInfo } else { text = presentationData.strings.Group_Username_RemoveExistingUsernamesOrExtendInfo("\(premiumLimits.maxPublicLinksCount)").string } } entries.append(.linksLimitInfo(presentationData.theme, text, count, limits.maxPublicLinksCount, premiumLimits.maxPublicLinksCount, isPremiumDisabled)) if let publicChannelsToRevoke = publicChannelsToRevoke { var index: Int32 = 0 for peer in publicChannelsToRevoke.sorted(by: { lhs, rhs in var lhsDate: Int32 = 0 var rhsDate: Int32 = 0 if case let .channel(lhs) = lhs { lhsDate = lhs.creationDate } if case let .channel(rhs) = rhs { rhsDate = rhs.creationDate } return lhsDate > rhsDate }) { entries.append(.existingLinkPeerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peer, ItemListPeerItemEditing(editable: true, editing: true, revealed: state.revealedRevokePeerId == peer.id), state.revokingPeerId == nil)) index += 1 } } } else { switch mode { case .revokeNames: break case .privateLink: let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_InviteLink.uppercased())) entries.append(.privateLink(presentationData.theme, invite, importers?.importers.prefix(3).compactMap { $0.peer.peer.flatMap(EnginePeer.init) } ?? [], importers?.count ?? 0, !isInitialSetup)) entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_Help)) switch mode { case .initialSetup, .revokeNames: break case .generic, .privateLink: entries.append(.privateLinkManage(presentationData.theme, presentationData.strings.InviteLink_Manage)) entries.append(.privateLinkManageInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo)) } case .generic, .initialSetup: let selectedType: CurrentChannelType if let current = state.selectedType { selectedType = current } else { selectedType = .privateChannel } let currentUsername: String if let current = state.editingPublicLinkText { currentUsername = current } else { currentUsername = "" } entries.append(.typeHeader(presentationData.theme, presentationData.strings.Group_Setup_TypeHeader.uppercased())) entries.append(.typePublic(presentationData.theme, presentationData.strings.Channel_Setup_TypePublic, selectedType == .publicChannel)) entries.append(.typePrivate(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivate, selectedType == .privateChannel)) switch selectedType { case .publicChannel: entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePublicHelp)) case .privateChannel: entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePrivateHelp)) } switch selectedType { case .publicChannel: entries.append(.editablePublicLink(presentationData.theme, presentationData.strings, "", currentUsername)) if let status = state.addressNameValidationStatus { let text: String switch status { case let .invalidFormat(error): switch error { case .startsWithDigit: text = presentationData.strings.Group_Username_InvalidStartsWithNumber case .startsWithUnderscore: text = presentationData.strings.Channel_Username_InvalidStartsWithUnderscore case .endsWithUnderscore: text = presentationData.strings.Channel_Username_InvalidEndsWithUnderscore case .tooShort: text = presentationData.strings.Group_Username_InvalidTooShort case .invalidCharacters: text = presentationData.strings.Channel_Username_InvalidCharacters } case let .availability(availability): switch availability { case .available: text = presentationData.strings.Channel_Username_UsernameIsAvailable(currentUsername).string case .invalid: text = presentationData.strings.Channel_Username_InvalidCharacters case .taken: text = presentationData.strings.Channel_Username_InvalidTaken case .purchaseAvailable: var markdownString = presentationData.strings.Channel_Username_UsernamePurchaseAvailable let entities = generateTextEntities(markdownString, enabledTypes: [.mention]) if let entity = entities.first { markdownString.insert(contentsOf: "]()", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.upperBound)) markdownString.insert(contentsOf: "[", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.lowerBound)) } text = markdownString } case .checking: text = presentationData.strings.Channel_Username_CheckingUsername } entries.append(.publicLinkStatus(presentationData.theme, text, status, currentUsername)) } entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp)) case .privateChannel: let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_InviteLink.uppercased())) entries.append(.privateLink(presentationData.theme, invite, importers?.importers.prefix(3).compactMap { $0.peer.peer.flatMap(EnginePeer.init) } ?? [], importers?.count ?? 0, !isInitialSetup)) entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp)) switch mode { case .initialSetup, .revokeNames: break case .generic, .privateLink: entries.append(.privateLinkManage(presentationData.theme, presentationData.strings.InviteLink_Manage)) entries.append(.privateLinkManageInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo)) } } } let forwardingEnabled: Bool if let enabled = state.forwardingEnabled { forwardingEnabled = enabled } else { if peer.flags.contains(.copyProtectionEnabled) { forwardingEnabled = false } else { forwardingEnabled = true } } entries.append(.forwardingHeader(presentationData.theme, presentationData.strings.Group_Setup_ForwardingGroupTitle.uppercased())) entries.append(.forwardingDisabled(presentationData.theme, presentationData.strings.Group_Setup_ForwardingDisabled, !forwardingEnabled)) entries.append(.forwardingInfo(presentationData.theme, forwardingEnabled ? presentationData.strings.Group_Setup_ForwardingGroupInfo : presentationData.strings.Group_Setup_ForwardingGroupInfoDisabled)) } } return entries } private func effectiveChannelType(mode: ChannelVisibilityControllerMode, state: ChannelVisibilityControllerState, peer: TelegramChannel, cachedData: CachedPeerData?) -> CurrentChannelType { let selectedType: CurrentChannelType if let current = state.selectedType { selectedType = current } else { if let addressName = peer.addressName, !addressName.isEmpty { selectedType = .publicChannel } else if let cachedChannelData = cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil { selectedType = .publicChannel } else if case .initialSetup = mode { selectedType = .publicChannel } else { selectedType = .privateChannel } } return selectedType } private func updatedAddressName(mode: ChannelVisibilityControllerMode, state: ChannelVisibilityControllerState, peer: EnginePeer, cachedData: CachedPeerData?) -> String? { if case let .channel(peer) = peer { let selectedType = effectiveChannelType(mode: mode, state: state, peer: peer, cachedData: cachedData) let currentUsername: String switch selectedType { case .privateChannel: currentUsername = "" case .publicChannel: if let current = state.editingPublicLinkText { currentUsername = current } else { if let username = peer.editableUsername { currentUsername = username } else { currentUsername = "" } } } if !currentUsername.isEmpty { if currentUsername != peer.editableUsername { return currentUsername } else { return nil } } else if peer.editableUsername != nil { return "" } else { return nil } } else if case .legacyGroup = peer { let currentUsername = state.editingPublicLinkText ?? "" if !currentUsername.isEmpty { return currentUsername } else { return nil } } else { return nil } } public enum ChannelVisibilityControllerMode { case initialSetup case generic case privateLink case revokeNames([EnginePeer]) } public func channelVisibilityController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, mode: ChannelVisibilityControllerMode, upgradedToSupergroup: @escaping (EnginePeer.Id, @escaping () -> Void) -> Void, onDismissRemoveController: ViewController? = nil, revokedPeerAddressName: ((EnginePeer.Id) -> Void)? = nil) -> ViewController { let statePromise = ValuePromise(ChannelVisibilityControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: ChannelVisibilityControllerState()) let updateState: ((ChannelVisibilityControllerState) -> ChannelVisibilityControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } let adminedPublicChannels = Promise<[EnginePeer]?>() if case let .revokeNames(peers) = mode { adminedPublicChannels.set(.single(peers)) } else { adminedPublicChannels.set(context.engine.peers.adminedPublicChannels(scope: .all) |> map { result in return result.map(\.peer) } |> map(Optional.init)) } let peersDisablingAddressNameAssignment = Promise<[EnginePeer]?>() peersDisablingAddressNameAssignment.set(.single(nil) |> then(context.engine.peers.channelAddressNameAssignmentAvailability(peerId: peerId.namespace == Namespaces.Peer.CloudChannel ? peerId : nil) |> mapToSignal { result -> Signal<[EnginePeer]?, NoError> in if case .addressNameLimitReached = result { return context.engine.peers.adminedPublicChannels(scope: .all) |> map { result in return result.map(\.peer) } |> map(Optional.init) } else { return .single([]) } })) var dismissImpl: (() -> Void)? var dismissInputImpl: (() -> Void)? var nextImpl: (() -> Void)? var scrollToPublicLinkTextImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var getControllerImpl: (() -> ViewController?)? var dismissTooltipsImpl: (() -> Void)? let actionsDisposable = DisposableSet() let checkAddressNameDisposable = MetaDisposable() actionsDisposable.add(checkAddressNameDisposable) let updateAddressNameDisposable = MetaDisposable() actionsDisposable.add(updateAddressNameDisposable) let revokeAddressNameDisposable = MetaDisposable() actionsDisposable.add(revokeAddressNameDisposable) let deactivateAllAddressNamesDisposable = MetaDisposable() actionsDisposable.add(deactivateAllAddressNamesDisposable) let revokeLinkDisposable = MetaDisposable() actionsDisposable.add(revokeLinkDisposable) let toggleCopyProtectionDisposable = MetaDisposable() actionsDisposable.add(toggleCopyProtectionDisposable) let toggleJoinToSendDisposable = MetaDisposable() actionsDisposable.add(toggleJoinToSendDisposable) let toggleRequestToJoinDisposable = MetaDisposable() actionsDisposable.add(toggleRequestToJoinDisposable) let temporaryOrder = Promise<[String]?>(nil) let arguments = ChannelVisibilityControllerArguments(context: context, updateCurrentType: { type in if type == .publicChannel { let _ = combineLatest( queue: Queue.mainQueue(), adminedPublicChannels.get() |> filter { $0 != nil } |> take(1), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) ) ).start(next: { peers, accountPeer, peer, data in let (limits, premiumLimits) = data let isPremium = accountPeer?.isPremium ?? false let hasAdditionalUsernames = (peer?._asPeer().usernames.firstIndex(where: { !$0.flags.contains(.isEditable) }) ?? nil) != nil if let peers = peers { let count = Int32(peers.count) if count < limits.maxPublicLinksCount || (count < premiumLimits.maxPublicLinksCount && isPremium) || hasAdditionalUsernames { updateState { state in return state.withUpdatedSelectedType(type) } } else { let controller = channelVisibilityController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, mode: .revokeNames(peers), upgradedToSupergroup: { _, _ in }, revokedPeerAddressName: { revokedPeerId in let updatedPublicChannels = peers.filter { $0.id != revokedPeerId } adminedPublicChannels.set(.single(updatedPublicChannels) |> then( context.engine.peers.adminedPublicChannels(scope: .all) |> map { result in return result.map(\.peer) } |> map(Optional.init)) ) }) controller.navigationPresentation = .modal pushControllerImpl?(controller) } } else { } }) } else { updateState { state in return state.withUpdatedSelectedType(type) } } }, updatePublicLinkText: { currentText, text in if text.isEmpty { checkAddressNameDisposable.set(nil) updateState { state in return state.withUpdatedEditingPublicLinkText(text).withUpdatedAddressNameValidationStatus(nil) } } else if currentText == text { checkAddressNameDisposable.set(nil) updateState { state in return state.withUpdatedEditingPublicLinkText(text).withUpdatedAddressNameValidationStatus(nil).withUpdatedAddressNameValidationStatus(nil) } } else { updateState { state in return state.withUpdatedEditingPublicLinkText(text) } checkAddressNameDisposable.set((context.engine.peers.validateAddressNameInteractive(domain: .peer(peerId), name: text) |> deliverOnMainQueue).start(next: { result in updateState { state in return state.withUpdatedAddressNameValidationStatus(result) } })) } }, scrollToPublicLinkText: { scrollToPublicLinkTextImpl?() }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in if (peerId == nil && fromPeerId == state.revealedRevokePeerId) || (peerId != nil && fromPeerId == nil) { return state.withUpdatedRevealedRevokePeerId(peerId) } else { return state } } }, revokePeerId: { peerId in updateState { state in return state.withUpdatedRevokingPeerId(peerId) } revokeAddressNameDisposable.set((context.engine.peers.updateAddressName(domain: .peer(peerId), name: nil) |> deliverOnMainQueue).start(error: { _ in updateState { state in return state.withUpdatedRevokingPeerId(nil) } }, completed: { revokedPeerAddressName?(peerId) dismissImpl?() })) }, copyLink: { invite in UIPasteboard.general.string = invite.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) }, shareLink: { invite in guard let inviteLink = invite.link else { return } let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData) 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) }, linkContextAction: { node, gesture in guard let node = node as? ContextReferenceContentNode, let controller = getControllerImpl?() else { return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } var items: [ContextMenuItem] = [] 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) let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: peerId)) |> deliverOnMainQueue).start(next: { exportedInvitation in if let link = exportedInvitation?.link { 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) } }) }))) 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) let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: peerId)) |> deliverOnMainQueue).start(next: { invite in if let invite = invite { let _ = (context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in let isGroup: Bool if let peer = peer as? TelegramChannel, case .broadcast = peer.info { isGroup = false } else { isGroup = true } presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), 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.actionSheet.destructiveActionTextColor) }, action: { _, f in f(.dismissWithoutContent) let _ = (context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in let isGroup: Bool if let peer = peer as? TelegramChannel, case .broadcast = peer.info { isGroup = false } else { isGroup = true } let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetTextItem(title: isGroup ? presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text : presentationData.strings.ChannelInfo_InviteLink_RevokeAlert_Text), ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { dismissAction() let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: peerId)) |> deliverOnMainQueue).start(next: { exportedInvitation in if let link = exportedInvitation?.link { var revoke = false updateState { state in if !state.revokingPrivateLink { revoke = true return state.withUpdatedRevokingPrivateLink(true) } else { return state } } if revoke { revokeLinkDisposable.set((context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: link) |> deliverOnMainQueue).start(completed: { updateState { $0.withUpdatedRevokingPrivateLink(false) } })) } } }) }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) }))) let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, manageInviteLinks: { let controller = inviteLinkListController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, admin: nil) pushControllerImpl?(controller) }, openLink: { invite in let controller = InviteLinkViewController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: invite, invitationsContext: nil, revokedInvitationsContext: nil, importersContext: nil) pushControllerImpl?(controller) }, toggleForwarding: { value in updateState { state in return state.withUpdatedForwardingEnabled(value) } }, updateJoinToSend: { value in updateState { state in return state.withUpdatedJoinToSend(value) } }, toggleApproveMembers: { value in updateState { state in return state.withUpdatedApproveMembers(value) } }, activateLink: { name in dismissInputImpl?() let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in let isGroup: Bool if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let title: String let text: String let action: String if isGroup { title = presentationData.strings.Group_Setup_ActivateAlertTitle text = presentationData.strings.Group_Setup_ActivateAlertText action = presentationData.strings.Group_Setup_ActivateAlertShow } else { title = presentationData.strings.Channel_Setup_ActivateAlertTitle text = presentationData.strings.Channel_Setup_ActivateAlertText action = presentationData.strings.Channel_Setup_ActivateAlertShow } presentControllerImpl?(textAlertController(context: context, title: title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: action, action: { let _ = (context.engine.peers.toggleAddressNameActive(domain: .peer(peerId), name: name, active: true) |> deliverOnMainQueue).start(error: { error in let errorText: String switch error { case .activeLimitReached: if isGroup { errorText = presentationData.strings.Group_Setup_ActiveLimitReachedError } else { errorText = presentationData.strings.Channel_Setup_ActiveLimitReachedError } default: errorText = presentationData.strings.Login_UnknownError } presentControllerImpl?(textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }) })]), nil) }) }, deactivateLink: { name in dismissInputImpl?() let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in let isGroup: Bool if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let title: String let text: String let action: String if isGroup { title = presentationData.strings.Group_Setup_DeactivateAlertTitle text = presentationData.strings.Group_Setup_DeactivateAlertText action = presentationData.strings.Group_Setup_DeactivateAlertHide } else { title = presentationData.strings.Channel_Setup_DeactivateAlertTitle text = presentationData.strings.Channel_Setup_DeactivateAlertText action = presentationData.strings.Channel_Setup_DeactivateAlertHide } presentControllerImpl?(textAlertController(context: context, title: title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: action, action: { let _ = context.engine.peers.toggleAddressNameActive(domain: .peer(peerId), name: name, active: false).start() })]), nil) }) }, openAuction: { username in dismissInputImpl?() context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://fragment.com/username/\(username)", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) }) let peerView = context.account.viewTracker.peerView(peerId) |> deliverOnMainQueue let previousHadNamesToRevoke = Atomic(value: nil) let previousInvitation = Atomic(value: nil) let previousUsernames = Atomic<[String]?>(value: nil) let mainLink = context.engine.data.subscribe( TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: peerId) ) let importersState = Promise(nil) let importersContext: Signal = mainLink |> distinctUntilChanged |> deliverOnMainQueue |> map { invite -> PeerInvitationImportersContext? in return invite.flatMap { context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .invite(invite: $0, requested: false)) } } |> afterNext { context in if let context = context { importersState.set(context.state |> map(Optional.init)) } else { importersState.set(.single(nil)) } } let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let signal = combineLatest( presentationData, statePromise.get() |> deliverOnMainQueue, peerView, adminedPublicChannels.get(), importersContext, importersState.get(), context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true), TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) ), temporaryOrder.get() ) |> deliverOnMainQueue |> map { presentationData, state, view, publicChannelsToRevoke, importersContext, importers, data, temporaryOrder -> (ItemListControllerState, (ItemListNodeState, Any)) in let peer = peerViewMainPeer(view) let (limits, premiumLimits, accountPeer) = data let isPremium = accountPeer?.isPremium ?? false var footerItem: ItemListControllerFooterItem? var isGroup = false if let peer = peer as? TelegramChannel { if case .group = peer.info { isGroup = true } } else if let _ = peer as? TelegramGroup { isGroup = true } var rightNavigationButton: ItemListNavigationButton? if case .revokeNames = mode { let count = Int32(publicChannelsToRevoke?.count ?? 0) if !premiumConfiguration.isPremiumDisabled && count < premiumLimits.maxPublicLinksCount { footerItem = IncreaseLimitFooterItem(theme: presentationData.theme, title: presentationData.strings.Premium_IncreaseLimit, colorful: true, action: { let controller = PremiumIntroScreen(context: context, source: .publicLinks) pushControllerImpl?(controller) }) } } else { if let peer = peer as? TelegramChannel { var doneEnabled = true if let selectedType = state.selectedType { switch selectedType { case .privateChannel: break case .publicChannel: var hasLocation = false if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil { hasLocation = true } if let addressNameValidationStatus = state.addressNameValidationStatus { switch addressNameValidationStatus { case .availability(.available): break default: doneEnabled = false } } else { doneEnabled = !(peer.addressName?.isEmpty ?? true) || hasLocation } } } let isInitialSetup: Bool if case .initialSetup = mode { isInitialSetup = true } else { isInitialSetup = false } rightNavigationButton = ItemListNavigationButton(content: .text(isInitialSetup ? presentationData.strings.Common_Next : presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { var updatedAddressNameValue: String? updateState { state in updatedAddressNameValue = updatedAddressName(mode: mode, state: state, peer: .channel(peer), cachedData: view.cachedData) return state } if let selectedType = state.selectedType, case .privateChannel = selectedType, peer.usernames.firstIndex(where: { $0.isActive && !$0.flags.contains(.isEditable) }) != nil { deactivateAllAddressNamesDisposable.set(context.engine.peers.deactivateAllAddressNames(peerId: peerId).start()) } if let updatedCopyProtection = state.forwardingEnabled { toggleCopyProtectionDisposable.set(context.engine.peers.toggleMessageCopyProtection(peerId: peerId, enabled: !updatedCopyProtection).start()) } if let updatedJoinToSend = state.joinToSend { toggleJoinToSendDisposable.set(context.engine.peers.toggleChannelJoinToSend(peerId: peerId, enabled: updatedJoinToSend == .members).start()) } if let updatedApproveMembers = state.approveMembers { toggleRequestToJoinDisposable.set(context.engine.peers.toggleChannelJoinRequest(peerId: peerId, enabled: updatedApproveMembers).start()) } if let updatedAddressNameValue = updatedAddressNameValue { let invokeAction: () -> Void = { updateState { state in return state.withUpdatedUpdatingAddressName(true) } _ = ApplicationSpecificNotice.markAsSeenSetPublicChannelLink(accountManager: context.sharedContext.accountManager).start() updateAddressNameDisposable.set((context.engine.peers.updateAddressName(domain: .peer(peerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(error: { _ in updateState { state in return state.withUpdatedUpdatingAddressName(false) } presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }, completed: { updateState { state in return state.withUpdatedUpdatingAddressName(false) } switch mode { case .initialSetup: nextImpl?() case .generic, .privateLink, .revokeNames: dismissImpl?() } })) } if !updatedAddressNameValue.isEmpty { _ = (ApplicationSpecificNotice.getSetPublicChannelLink(accountManager: context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { showAlert in if showAlert { let text: String if case .broadcast = peer.info { text = presentationData.strings.Channel_Edit_PrivatePublicLinkAlert } else { text = presentationData.strings.Group_Edit_PrivatePublicLinkAlert } presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: invokeAction)]), nil) } else { invokeAction() } }) } else { invokeAction() } } else { switch mode { case .initialSetup: nextImpl?() case .generic, .privateLink, .revokeNames: dismissImpl?() } } }) } else if let peer = peer as? TelegramGroup { var doneEnabled = true if let selectedType = state.selectedType { switch selectedType { case .privateChannel: break case .publicChannel: if let addressNameValidationStatus = state.addressNameValidationStatus { switch addressNameValidationStatus { case .availability(.available): break default: doneEnabled = false } } else { doneEnabled = !(peer.addressName?.isEmpty ?? true) } } } rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { var updatedAddressNameValue: String? updateState { state in updatedAddressNameValue = updatedAddressName(mode: mode, state: state, peer: .legacyGroup(peer), cachedData: nil) return state } if let updatedCopyProtection = state.forwardingEnabled { toggleCopyProtectionDisposable.set(context.engine.peers.toggleMessageCopyProtection(peerId: peerId, enabled: !updatedCopyProtection).start()) } if let updatedAddressNameValue = updatedAddressNameValue { let invokeAction: () -> Void = { updateState { state in return state.withUpdatedUpdatingAddressName(true) } _ = ApplicationSpecificNotice.markAsSeenSetPublicChannelLink(accountManager: context.sharedContext.accountManager).start() let signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) |> mapToSignal { upgradedPeerId -> Signal in return context.engine.peers.updateAddressName(domain: .peer(upgradedPeerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) |> `catch` { _ -> Signal in return .complete() } |> mapToSignal { _ -> Signal in return .complete() } |> then(.single(upgradedPeerId)) |> castError(ConvertGroupToSupergroupError.self) } |> deliverOnMainQueue updateAddressNameDisposable.set((signal |> deliverOnMainQueue).start(next: { updatedPeerId in if let updatedPeerId = updatedPeerId { upgradedToSupergroup(updatedPeerId, { dismissImpl?() }) } else { dismissImpl?() } }, error: { error in updateState { state in return state.withUpdatedUpdatingAddressName(false) } switch error { case .tooManyChannels: pushControllerImpl?(oldChannelsController(context: context, updatedPresentationData: updatedPresentationData, intent: .upgrade)) default: presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } })) } _ = (ApplicationSpecificNotice.getSetPublicChannelLink(accountManager: context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { showAlert in if showAlert { let text: String if isGroup { text = presentationData.strings.Group_Edit_PrivatePublicLinkAlert } else { text = presentationData.strings.Channel_Edit_PrivatePublicLinkAlert } presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: invokeAction)]), nil) } else { invokeAction() } }) } else { switch mode { case .initialSetup: nextImpl?() case .generic, .privateLink, .revokeNames: dismissImpl?() } } }) } } if state.revokingPeerId != nil { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } let leftNavigationButton: ItemListNavigationButton? switch mode { case .initialSetup: leftNavigationButton = nil case .generic, .privateLink, .revokeNames: leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) } var crossfade: Bool = false var animateChanges: Bool = false if let cachedData = view.cachedData as? CachedChannelData { let invitation = cachedData.exportedInvitation let previousInvitation = previousInvitation.swap(invitation) if invitation != previousInvitation { crossfade = true } } let hasNamesToRevoke = publicChannelsToRevoke != nil && !publicChannelsToRevoke!.isEmpty let hadNamesToRevoke = previousHadNamesToRevoke.swap(hasNamesToRevoke) if let peer = view.peers[view.peerId] as? TelegramChannel { let currentUsernames = peer.usernames.map { $0.username } let previousUsernames = previousUsernames.swap(currentUsernames) let selectedType: CurrentChannelType if case .privateLink = mode { selectedType = .privateChannel } else { if let current = state.selectedType { selectedType = current } else { if let addressName = peer.addressName, !addressName.isEmpty { selectedType = .publicChannel } else if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil { selectedType = .publicChannel } else { selectedType = .privateChannel } } } if selectedType == .publicChannel, let hadNamesToRevoke = hadNamesToRevoke, !crossfade { crossfade = hadNamesToRevoke != hasNamesToRevoke } if let hadNamesToRevoke = hadNamesToRevoke { animateChanges = hadNamesToRevoke != hasNamesToRevoke } if temporaryOrder != nil || previousUsernames != currentUsernames { animateChanges = true } } let title: String switch mode { case .generic, .initialSetup: if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil { title = presentationData.strings.Group_PublicLink_Title } else { title = isGroup ? presentationData.strings.GroupInfo_GroupType : presentationData.strings.Channel_TypeSetup_Title } case .privateLink: title = presentationData.strings.GroupInfo_InviteLink_Title case .revokeNames: title = presentationData.strings.Premium_LimitReached } let entries = channelVisibilityControllerEntries(presentationData: presentationData, mode: mode, view: view, publicChannelsToRevoke: publicChannelsToRevoke, importers: importers, state: state, limits: limits, premiumLimits: premiumLimits, isPremium: isPremium, isPremiumDisabled: premiumConfiguration.isPremiumDisabled, temporaryOrder: temporaryOrder) var focusItemTag: ItemListItemTag? if entries.count > 1, let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil { focusItemTag = ChannelVisibilityEntryTag.publicLink } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, focusItemTag: focusItemTag, footerItem: footerItem, crossfadeState: crossfade, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ItemListController(context: context, state: signal) controller.willDisappear = { _ in dismissTooltipsImpl?() } controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [ChannelVisibilityEntry]) -> Signal in let fromEntry = entries[fromIndex] guard case let .additionalLink(_, fromUsername, _) = fromEntry else { return .single(false) } var referenceId: String? var beforeAll = false var afterAll = false var maxIndex: Int? var currentUsernames: [String] = [] var i = 0 for entry in entries { switch entry { case let .additionalLink(_, link, _): currentUsernames.append(link.username) if !link.isActive && maxIndex == nil { maxIndex = max(0, i - 1) } i += 1 default: break } } if toIndex < entries.count { switch entries[toIndex] { case let .additionalLink(_, toUsername, _): if toUsername.isActive { referenceId = toUsername.username } else { afterAll = true } default: if entries[toIndex] < fromEntry { beforeAll = true } else { afterAll = true } } } else { afterAll = true } var previousIndex: Int? for i in 0 ..< currentUsernames.count { if currentUsernames[i] == fromUsername.username { previousIndex = i currentUsernames.remove(at: i) break } } var didReorder = false if let referenceId = referenceId { var inserted = false for i in 0 ..< currentUsernames.count { if currentUsernames[i] == referenceId { if fromIndex < toIndex { didReorder = previousIndex != i + 1 currentUsernames.insert(fromUsername.username, at: i + 1) } else { didReorder = previousIndex != i currentUsernames.insert(fromUsername.username, at: i) } inserted = true break } } if !inserted { didReorder = previousIndex != currentUsernames.count if let maxIndex = maxIndex { currentUsernames.insert(fromUsername.username, at: maxIndex) } else { currentUsernames.append(fromUsername.username) } } } else if beforeAll { didReorder = previousIndex != 0 currentUsernames.insert(fromUsername.username, at: 0) } else if afterAll { didReorder = previousIndex != currentUsernames.count if let maxIndex = maxIndex { currentUsernames.insert(fromUsername.username, at: maxIndex) } else { currentUsernames.append(fromUsername.username) } } temporaryOrder.set(.single(currentUsernames)) if didReorder { DispatchQueue.main.async { dismissInputImpl?() } } return .single(didReorder) }) controller.setReorderCompleted({ (entries: [ChannelVisibilityEntry]) -> Void in var currentUsernames: [TelegramPeerUsername] = [] for entry in entries { switch entry { case let .additionalLink(_, username, _): currentUsernames.append(username) default: break } } let _ = (context.engine.peers.reorderAddressNames(domain: .peer(peerId), names: currentUsernames) |> deliverOnMainQueue).start(completed: { temporaryOrder.set(.single(nil)) }) }) controller.beganInteractiveDragging = { dismissInputImpl?() } dismissImpl = { [weak controller, weak onDismissRemoveController] in guard let controller = controller else { return } controller.view.endEditing(true) if let onDismissRemoveController = onDismissRemoveController, let navigationController = controller.navigationController { navigationController.setViewControllers(navigationController.viewControllers.filter { c in if c === controller || c === onDismissRemoveController { return false } else { return true } }, animated: true) } else { controller.dismiss() } } dismissInputImpl = { [weak controller] in controller?.view.endEditing(true) } nextImpl = { [weak controller] in if let controller = controller { if case .initialSetup = mode { let selectionController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, mode: .channelCreation, onlyWriteable: true)) (controller.navigationController as? NavigationController)?.replaceAllButRootController(selectionController, animated: true) let _ = (selectionController.result |> deliverOnMainQueue).start(next: { [weak selectionController] result in guard let selectionController = selectionController, let navigationController = selectionController.navigationController as? NavigationController else { return } var peerIds: [ContactListPeerId] = [] if case let .result(peerIdsValue, _) = result { peerIds = peerIdsValue } let filteredPeerIds = peerIds.compactMap({ peerId -> PeerId? in if case let .peer(id) = peerId { return id } else { return nil } }) if filteredPeerIds.isEmpty { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer = peer else { return } context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), keepStack: .never, animated: true)) }) } else { selectionController.displayProgress = true let _ = (context.engine.peers.addChannelMembers(peerId: peerId, memberIds: filteredPeerIds) |> deliverOnMainQueue).start(error: { [weak selectionController] _ in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer = peer else { return } guard let selectionController = selectionController, let navigationController = selectionController.navigationController as? NavigationController else { return } context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), keepStack: .never, animated: true)) }) }, completed: { [weak selectionController] in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer = peer else { return } guard let selectionController = selectionController, let navigationController = selectionController.navigationController as? NavigationController else { return } context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), keepStack: .never, animated: true)) }) }) } }) } else { if let navigationController = controller.navigationController as? NavigationController { navigationController.replaceAllButRootController(context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil), animated: true) } } } } scrollToPublicLinkTextImpl = { [weak controller] in DispatchQueue.main.async { if let strongController = controller { var resultItemNode: ListViewItemNode? let _ = strongController.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListSingleLineInputItemNode { if let tag = itemNode.tag as? ChannelVisibilityEntryTag { if tag == .publicLink { resultItemNode = itemNode return true } } } return false }) if let resultItemNode = resultItemNode { strongController.ensureItemNodeVisible(resultItemNode) } } } } presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } pushControllerImpl = { [weak controller] c in controller?.push(c) } presentInGlobalOverlayImpl = { [weak controller] c in if let controller = controller { controller.presentInGlobalOverlay(c) } } getControllerImpl = { [weak controller] in return controller } dismissTooltipsImpl = { [weak controller] in controller?.window?.forEachController({ controller in if let controller = controller as? UndoOverlayController { controller.dismissWithCommitAction() } }) controller?.forEachController({ controller in if let controller = controller as? UndoOverlayController { controller.dismissWithCommitAction() } return true }) } return controller } final class InviteLinkContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController private let sourceNode: ContextReferenceContentNode init(controller: ViewController, sourceNode: ContextReferenceContentNode) { self.controller = controller self.sourceNode = sourceNode } func transitionInfo() -> ContextControllerReferenceViewInfo? { return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) } }