mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
1467 lines
78 KiB
Swift
1467 lines
78 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SyncCore
|
|
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
|
|
|
|
private final class ChannelVisibilityControllerArguments {
|
|
let context: AccountContext
|
|
let updateCurrentType: (CurrentChannelType) -> Void
|
|
let updatePublicLinkText: (String?, String) -> Void
|
|
let scrollToPublicLinkText: () -> Void
|
|
let displayPrivateLinkMenu: (String) -> Void
|
|
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
|
|
let revokePeerId: (PeerId) -> Void
|
|
let copyLink: (ExportedInvitation) -> Void
|
|
let shareLink: (ExportedInvitation) -> Void
|
|
let linkContextAction: (ASDisplayNode) -> Void
|
|
let manageInviteLinks: () -> Void
|
|
|
|
init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, linkContextAction: @escaping (ASDisplayNode) -> Void, manageInviteLinks: @escaping () -> Void) {
|
|
self.context = context
|
|
self.updateCurrentType = updateCurrentType
|
|
self.updatePublicLinkText = updatePublicLinkText
|
|
self.scrollToPublicLinkText = scrollToPublicLinkText
|
|
self.displayPrivateLinkMenu = displayPrivateLinkMenu
|
|
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
|
self.revokePeerId = revokePeerId
|
|
self.copyLink = copyLink
|
|
self.shareLink = shareLink
|
|
self.linkContextAction = linkContextAction
|
|
self.manageInviteLinks = manageInviteLinks
|
|
}
|
|
}
|
|
|
|
private enum ChannelVisibilitySection: Int32 {
|
|
case type
|
|
case link
|
|
case linkActions
|
|
}
|
|
|
|
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 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 editablePublicLink(PresentationTheme, PresentationStrings, String, String)
|
|
case privateLinkHeader(PresentationTheme, String)
|
|
case privateLink(PresentationTheme, ExportedInvitation?, Bool)
|
|
case privateLinkInfo(PresentationTheme, String)
|
|
case privateLinkManage(PresentationTheme, String)
|
|
case privateLinkManageInfo(PresentationTheme, String)
|
|
|
|
case publicLinkInfo(PresentationTheme, String)
|
|
case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus)
|
|
|
|
case existingLinksInfo(PresentationTheme, String)
|
|
case existingLinkPeerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, ItemListPeerItemEditing, Bool)
|
|
|
|
var section: ItemListSectionId {
|
|
switch self {
|
|
case .typeHeader, .typePublic, .typePrivate, .typeInfo:
|
|
return ChannelVisibilitySection.type.rawValue
|
|
case .publicLinkHeader, .publicLinkAvailability, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus:
|
|
return ChannelVisibilitySection.link.rawValue
|
|
case .privateLinkManage, .privateLinkManageInfo:
|
|
return ChannelVisibilitySection.linkActions.rawValue
|
|
case .existingLinksInfo, .existingLinkPeerItem:
|
|
return ChannelVisibilitySection.link.rawValue
|
|
}
|
|
}
|
|
|
|
var stableId: Int32 {
|
|
switch self {
|
|
case .typeHeader:
|
|
return 0
|
|
case .typePublic:
|
|
return 1
|
|
case .typePrivate:
|
|
return 2
|
|
case .typeInfo:
|
|
return 3
|
|
case .publicLinkHeader:
|
|
return 4
|
|
case .publicLinkAvailability:
|
|
return 5
|
|
case .privateLinkHeader:
|
|
return 6
|
|
case .privateLink:
|
|
return 7
|
|
case .editablePublicLink:
|
|
return 8
|
|
case .privateLinkInfo:
|
|
return 9
|
|
case .publicLinkStatus:
|
|
return 10
|
|
case .publicLinkInfo:
|
|
return 11
|
|
case .existingLinksInfo:
|
|
return 12
|
|
case let .existingLinkPeerItem(index, _, _, _, _, _, _, _):
|
|
return 13 + index
|
|
case .privateLinkManage:
|
|
return 1000
|
|
case .privateLinkManageInfo:
|
|
return 1001
|
|
}
|
|
}
|
|
|
|
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 .privateLinkHeader(lhsTheme, lhsTitle):
|
|
if case let .privateLinkHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .privateLink(lhsTheme, lhsInvite, lhsDisplayImporters):
|
|
if case let .privateLink(rhsTheme, rhsInvite, rhsDisplayImporters) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite, 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):
|
|
if case let .publicLinkStatus(rhsTheme, rhsText, rhsStatus) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStatus == rhsStatus {
|
|
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.isEqual(rhsPeer) {
|
|
return false
|
|
}
|
|
if lhsEditing != rhsEditing {
|
|
return false
|
|
}
|
|
if lhsEnabled != rhsEnabled {
|
|
return false
|
|
}
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
static func <(lhs: ChannelVisibilityEntry, rhs: ChannelVisibilityEntry) -> Bool {
|
|
return lhs.stableId < rhs.stableId
|
|
}
|
|
|
|
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(theme, text, value):
|
|
let attr = NSMutableAttributedString(string: text, textColor: value ? theme.list.freeTextColor : theme.list.freeTextErrorColor)
|
|
attr.addAttribute(.font, value: Font.regular(13), range: NSMakeRange(0, attr.length))
|
|
return ItemListActivityTextItem(displayActivity: value, presentationData: presentationData, text: attr, sectionId: self.section)
|
|
case let .privateLinkHeader(_, title):
|
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
|
|
case let .privateLink(_, invite, displayImporters):
|
|
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: 0, 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 in
|
|
arguments.linkContextAction(node)
|
|
}, viewAction: {
|
|
|
|
})
|
|
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(theme, text, status):
|
|
var displayActivity = false
|
|
let color: UIColor
|
|
switch status {
|
|
case .invalidFormat:
|
|
color = theme.list.freeTextErrorColor
|
|
case let .availability(availability):
|
|
switch availability {
|
|
case .available:
|
|
color = theme.list.freeTextSuccessColor
|
|
case .invalid:
|
|
color = theme.list.freeTextErrorColor
|
|
case .taken:
|
|
color = theme.list.freeTextErrorColor
|
|
}
|
|
case .checking:
|
|
color = theme.list.freeTextColor
|
|
displayActivity = true
|
|
}
|
|
return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: NSAttributedString(string: text, textColor: color), 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)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum CurrentChannelType {
|
|
case publicChannel
|
|
case privateChannel
|
|
}
|
|
|
|
private enum CurrentChannelLocation: Equatable {
|
|
case removed
|
|
case location(PeerGeoLocation)
|
|
}
|
|
|
|
private struct ChannelVisibilityControllerState: Equatable {
|
|
let selectedType: CurrentChannelType?
|
|
let editingPublicLinkText: String?
|
|
let addressNameValidationStatus: AddressNameValidationStatus?
|
|
let updatingAddressName: Bool
|
|
let revealedRevokePeerId: PeerId?
|
|
let revokingPeerId: PeerId?
|
|
let revokingPrivateLink: Bool
|
|
|
|
init() {
|
|
self.selectedType = nil
|
|
self.editingPublicLinkText = nil
|
|
self.addressNameValidationStatus = nil
|
|
self.updatingAddressName = false
|
|
self.revealedRevokePeerId = nil
|
|
self.revokingPeerId = nil
|
|
self.revokingPrivateLink = false
|
|
}
|
|
|
|
init(selectedType: CurrentChannelType?, editingPublicLinkText: String?, addressNameValidationStatus: AddressNameValidationStatus?, updatingAddressName: Bool, revealedRevokePeerId: PeerId?, revokingPeerId: PeerId?, revokingPrivateLink: Bool) {
|
|
self.selectedType = selectedType
|
|
self.editingPublicLinkText = editingPublicLinkText
|
|
self.addressNameValidationStatus = addressNameValidationStatus
|
|
self.updatingAddressName = updatingAddressName
|
|
self.revealedRevokePeerId = revealedRevokePeerId
|
|
self.revokingPeerId = revokingPeerId
|
|
self.revokingPrivateLink = revokingPrivateLink
|
|
}
|
|
|
|
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
|
|
}
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func withUpdatedRevealedRevokePeerId(_ revealedRevokePeerId: PeerId?) -> ChannelVisibilityControllerState {
|
|
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink)
|
|
}
|
|
|
|
func withUpdatedRevokingPeerId(_ revokingPeerId: PeerId?) -> ChannelVisibilityControllerState {
|
|
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: revokingPeerId, revokingPrivateLink: self.revokingPrivateLink)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
private func channelVisibilityControllerEntries(presentationData: PresentationData, mode: ChannelVisibilityControllerMode, view: PeerView, publicChannelsToRevoke: [Peer]?, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] {
|
|
var entries: [ChannelVisibilityEntry] = []
|
|
|
|
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 = .publicChannel
|
|
} else {
|
|
selectedType = .privateChannel
|
|
}
|
|
}
|
|
}
|
|
|
|
let currentAddressName: String
|
|
if let current = state.editingPublicLinkText {
|
|
currentAddressName = current
|
|
} else {
|
|
if let addressName = peer.addressName {
|
|
currentAddressName = addressName
|
|
} else {
|
|
currentAddressName = ""
|
|
}
|
|
}
|
|
|
|
if let _ = (view.cachedData as? CachedChannelData)?.peerGeoLocation {
|
|
} else {
|
|
switch mode {
|
|
case .privateLink:
|
|
break
|
|
case .initialSetup, .generic:
|
|
entries.append(.typeHeader(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_TypeHeader : presentationData.strings.Channel_Edit_LinkItem))
|
|
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:
|
|
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))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
switch selectedType {
|
|
case .publicChannel:
|
|
var displayAvailability = false
|
|
if peer.addressName == nil {
|
|
displayAvailability = publicChannelsToRevoke != nil && !(publicChannelsToRevoke!.isEmpty)
|
|
}
|
|
|
|
if displayAvailability {
|
|
if let publicChannelsToRevoke = publicChannelsToRevoke {
|
|
entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_RemoveExistingUsernamesInfo, false))
|
|
var index: Int32 = 0
|
|
for peer in publicChannelsToRevoke.sorted(by: { lhs, rhs in
|
|
var lhsDate: Int32 = 0
|
|
var rhsDate: Int32 = 0
|
|
if let lhs = lhs as? TelegramChannel {
|
|
lhsDate = lhs.creationDate
|
|
}
|
|
if let rhs = rhs as? TelegramChannel {
|
|
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 {
|
|
entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp, true))
|
|
}
|
|
} else {
|
|
entries.append(.editablePublicLink(presentationData.theme, presentationData.strings, presentationData.strings.Group_PublicLink_Placeholder, currentAddressName))
|
|
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:
|
|
text = presentationData.strings.Channel_Username_InvalidCharacters
|
|
case .endsWithUnderscore:
|
|
text = presentationData.strings.Channel_Username_InvalidCharacters
|
|
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(currentAddressName).0
|
|
case .invalid:
|
|
text = presentationData.strings.Channel_Username_InvalidCharacters
|
|
case .taken:
|
|
text = presentationData.strings.Channel_Username_InvalidTaken
|
|
}
|
|
case .checking:
|
|
text = presentationData.strings.Channel_Username_CheckingUsername
|
|
}
|
|
|
|
entries.append(.publicLinkStatus(presentationData.theme, text, status))
|
|
}
|
|
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))
|
|
}
|
|
switch mode {
|
|
case .initialSetup:
|
|
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, mode != .initialSetup))
|
|
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:
|
|
break
|
|
case .generic, .privateLink:
|
|
entries.append(.privateLinkManage(presentationData.theme, presentationData.strings.InviteLink_Manage))
|
|
entries.append(.privateLinkManageInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo))
|
|
}
|
|
}
|
|
} else if let _ = view.peers[view.peerId] as? TelegramGroup {
|
|
switch mode {
|
|
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, mode != .initialSetup))
|
|
entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_Help))
|
|
switch mode {
|
|
case .initialSetup:
|
|
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 currentAddressName: String
|
|
if let current = state.editingPublicLinkText {
|
|
currentAddressName = current
|
|
} else {
|
|
currentAddressName = ""
|
|
}
|
|
|
|
entries.append(.typeHeader(presentationData.theme, presentationData.strings.Group_Setup_TypeHeader))
|
|
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:
|
|
let displayAvailability = publicChannelsToRevoke == nil || !(publicChannelsToRevoke!.isEmpty)
|
|
|
|
if displayAvailability {
|
|
if let publicChannelsToRevoke = publicChannelsToRevoke {
|
|
entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_RemoveExistingUsernamesInfo, false))
|
|
var index: Int32 = 0
|
|
for peer in publicChannelsToRevoke.sorted(by: { lhs, rhs in
|
|
var lhsDate: Int32 = 0
|
|
var rhsDate: Int32 = 0
|
|
if let lhs = lhs as? TelegramChannel {
|
|
lhsDate = lhs.creationDate
|
|
}
|
|
if let rhs = rhs as? TelegramChannel {
|
|
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 {
|
|
entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp, true))
|
|
}
|
|
} else {
|
|
entries.append(.editablePublicLink(presentationData.theme, presentationData.strings, "", currentAddressName))
|
|
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_InvalidCharacters
|
|
case .endsWithUnderscore:
|
|
text = presentationData.strings.Channel_Username_InvalidCharacters
|
|
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(currentAddressName).0
|
|
case .invalid:
|
|
text = presentationData.strings.Channel_Username_InvalidCharacters
|
|
case .taken:
|
|
text = presentationData.strings.Channel_Username_InvalidTaken
|
|
}
|
|
case .checking:
|
|
text = presentationData.strings.Channel_Username_CheckingUsername
|
|
}
|
|
|
|
entries.append(.publicLinkStatus(presentationData.theme, text, status))
|
|
}
|
|
|
|
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, mode != .initialSetup))
|
|
entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp))
|
|
switch mode {
|
|
case .initialSetup:
|
|
break
|
|
case .generic, .privateLink:
|
|
entries.append(.privateLinkManage(presentationData.theme, presentationData.strings.InviteLink_Manage))
|
|
entries.append(.privateLinkManageInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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: Peer, cachedData: CachedPeerData?) -> String? {
|
|
if let peer = peer as? TelegramChannel {
|
|
let selectedType = effectiveChannelType(mode: mode, state: state, peer: peer, cachedData: cachedData)
|
|
|
|
let currentAddressName: String
|
|
|
|
switch selectedType {
|
|
case .privateChannel:
|
|
currentAddressName = ""
|
|
case .publicChannel:
|
|
if let current = state.editingPublicLinkText {
|
|
currentAddressName = current
|
|
} else {
|
|
if let addressName = peer.addressName {
|
|
currentAddressName = addressName
|
|
} else {
|
|
currentAddressName = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
if !currentAddressName.isEmpty {
|
|
if currentAddressName != peer.addressName {
|
|
return currentAddressName
|
|
} else {
|
|
return nil
|
|
}
|
|
} else if peer.addressName != nil {
|
|
return ""
|
|
} else {
|
|
return nil
|
|
}
|
|
} else if let _ = peer as? TelegramGroup {
|
|
let currentAddressName = state.editingPublicLinkText ?? ""
|
|
if !currentAddressName.isEmpty {
|
|
return currentAddressName
|
|
} else {
|
|
return nil
|
|
}
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public enum ChannelVisibilityControllerMode {
|
|
case initialSetup
|
|
case generic
|
|
case privateLink
|
|
}
|
|
|
|
public func channelVisibilityController(context: AccountContext, peerId: PeerId, mode: ChannelVisibilityControllerMode, upgradedToSupergroup: @escaping (PeerId, @escaping () -> Void) -> Void, onDismissRemoveController: ViewController? = 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 peersDisablingAddressNameAssignment = Promise<[Peer]?>()
|
|
peersDisablingAddressNameAssignment.set(.single(nil) |> then(channelAddressNameAssignmentAvailability(account: context.account, peerId: peerId.namespace == Namespaces.Peer.CloudChannel ? peerId : nil) |> mapToSignal { result -> Signal<[Peer]?, NoError> in
|
|
if case .addressNameLimitReached = result {
|
|
return adminedPublicChannels(account: context.account, location: false)
|
|
|> map(Optional.init)
|
|
} else {
|
|
return .single([])
|
|
}
|
|
}))
|
|
|
|
var dismissImpl: (() -> Void)?
|
|
var nextImpl: (() -> Void)?
|
|
var displayPrivateLinkMenuImpl: ((String) -> 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 revokeLinkDisposable = MetaDisposable()
|
|
actionsDisposable.add(revokeLinkDisposable)
|
|
|
|
let arguments = ChannelVisibilityControllerArguments(context: context, updateCurrentType: { type in
|
|
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((validateAddressNameInteractive(account: context.account, domain: .peer(peerId), name: text)
|
|
|> deliverOnMainQueue).start(next: { result in
|
|
updateState { state in
|
|
return state.withUpdatedAddressNameValidationStatus(result)
|
|
}
|
|
}))
|
|
}
|
|
}, scrollToPublicLinkText: {
|
|
scrollToPublicLinkTextImpl?()
|
|
}, displayPrivateLinkMenu: { text in
|
|
displayPrivateLinkMenuImpl?(text)
|
|
}, 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((updateAddressName(account: context.account, domain: .peer(peerId), name: nil) |> deliverOnMainQueue).start(error: { _ in
|
|
updateState { state in
|
|
return state.withUpdatedRevokingPeerId(nil)
|
|
}
|
|
}, completed: {
|
|
peersDisablingAddressNameAssignment.set(.single([]) |> delay(0.2, queue: Queue.mainQueue()) |> afterNext { _ in
|
|
updateState { state in
|
|
return state.withUpdatedRevokingPeerId(nil)
|
|
}
|
|
})
|
|
}))
|
|
}, copyLink: { invite in
|
|
UIPasteboard.general.string = invite.link
|
|
|
|
dismissTooltipsImpl?()
|
|
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
|
}, shareLink: { invite in
|
|
let shareController = ShareController(context: context, subject: .url(invite.link))
|
|
presentControllerImpl?(shareController, nil)
|
|
}, linkContextAction: { node in
|
|
guard let node = node as? ContextExtractedContentContainingNode, 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.account.postbox.transaction { transaction -> String? in
|
|
if let cachedData = transaction.getPeerCachedData(peerId: peerId) {
|
|
if let cachedData = cachedData as? CachedChannelData {
|
|
return cachedData.exportedInvitation?.link
|
|
} else if let cachedData = cachedData as? CachedGroupData {
|
|
return cachedData.exportedInvitation?.link
|
|
}
|
|
}
|
|
return nil
|
|
} |> deliverOnMainQueue).start(next: { link in
|
|
if let link = link {
|
|
UIPasteboard.general.string = link
|
|
|
|
dismissTooltipsImpl?()
|
|
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(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: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
|
|
}, action: { _, f in
|
|
f(.dismissWithoutContent)
|
|
|
|
let _ = (context.account.postbox.transaction { transaction -> ExportedInvitation? in
|
|
if let cachedData = transaction.getPeerCachedData(peerId: peerId) {
|
|
if let cachedData = cachedData as? CachedChannelData {
|
|
return cachedData.exportedInvitation
|
|
} else if let cachedData = cachedData as? CachedGroupData {
|
|
return cachedData.exportedInvitation
|
|
}
|
|
}
|
|
return nil
|
|
} |> 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
|
|
}
|
|
let controller = InviteLinkQRCodeController(context: context, invite: invite, isGroup: isGroup)
|
|
presentControllerImpl?(controller, 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 controller = ActionSheetController(presentationData: presentationData)
|
|
let dismissAction: () -> Void = { [weak controller] in
|
|
controller?.dismissAnimated()
|
|
}
|
|
controller.setItemGroups([
|
|
ActionSheetItemGroup(items: [
|
|
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
|
|
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
|
|
dismissAction()
|
|
|
|
let _ = (context.account.postbox.transaction { transaction -> String? in
|
|
if let cachedData = transaction.getPeerCachedData(peerId: peerId) {
|
|
if let cachedData = cachedData as? CachedChannelData {
|
|
return cachedData.exportedInvitation?.link
|
|
} else if let cachedData = cachedData as? CachedGroupData {
|
|
return cachedData.exportedInvitation?.link
|
|
}
|
|
}
|
|
return nil
|
|
} |> deliverOnMainQueue).start(next: { link in
|
|
if let link = link {
|
|
var revoke = false
|
|
updateState { state in
|
|
if !state.revokingPrivateLink {
|
|
revoke = true
|
|
return state.withUpdatedRevokingPrivateLink(true)
|
|
} else {
|
|
return state
|
|
}
|
|
}
|
|
if revoke {
|
|
revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, 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(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: nil)
|
|
presentInGlobalOverlayImpl?(contextController)
|
|
}, manageInviteLinks: {
|
|
let controller = inviteLinkListController(context: context, peerId: peerId, admin: nil)
|
|
pushControllerImpl?(controller)
|
|
})
|
|
|
|
let peerView = context.account.viewTracker.peerView(peerId)
|
|
|> deliverOnMainQueue
|
|
|
|
let previousHadNamesToRevoke = Atomic<Bool?>(value: nil)
|
|
|
|
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, peerView, peersDisablingAddressNameAssignment.get() |> deliverOnMainQueue)
|
|
|> deliverOnMainQueue
|
|
|> map { presentationData, state, view, publicChannelsToRevoke -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
|
let peer = peerViewMainPeer(view)
|
|
|
|
var rightNavigationButton: ItemListNavigationButton?
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
rightNavigationButton = ItemListNavigationButton(content: .text(mode == .initialSetup ? 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: peer, cachedData: view.cachedData)
|
|
return state
|
|
}
|
|
|
|
if let updatedAddressNameValue = updatedAddressNameValue {
|
|
let invokeAction: () -> Void = {
|
|
updateState { state in
|
|
return state.withUpdatedUpdatingAddressName(true)
|
|
}
|
|
_ = ApplicationSpecificNotice.markAsSeenSetPublicChannelLink(accountManager: context.sharedContext.accountManager).start()
|
|
|
|
updateAddressNameDisposable.set((updateAddressName(account: context.account, 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, 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:
|
|
dismissImpl?()
|
|
}
|
|
}))
|
|
|
|
}
|
|
|
|
_ = (ApplicationSpecificNotice.getSetPublicChannelLink(accountManager: context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { showAlert in
|
|
if showAlert {
|
|
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_Edit_PrivatePublicLinkAlert, 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:
|
|
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: peer, cachedData: nil)
|
|
return state
|
|
}
|
|
|
|
if let updatedAddressNameValue = updatedAddressNameValue {
|
|
let invokeAction: () -> Void = {
|
|
updateState { state in
|
|
return state.withUpdatedUpdatingAddressName(true)
|
|
}
|
|
_ = ApplicationSpecificNotice.markAsSeenSetPublicChannelLink(accountManager: context.sharedContext.accountManager).start()
|
|
|
|
let signal = convertGroupToSupergroup(account: context.account, peerId: peerId)
|
|
|> mapToSignal { upgradedPeerId -> Signal<PeerId?, ConvertGroupToSupergroupError> in
|
|
return updateAddressName(account: context.account, domain: .peer(upgradedPeerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue)
|
|
|> `catch` { _ -> Signal<Void, NoError> in
|
|
return .complete()
|
|
}
|
|
|> mapToSignal { _ -> Signal<PeerId?, NoError> 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, intent: .upgrade))
|
|
default:
|
|
presentControllerImpl?(textAlertController(context: context, 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 {
|
|
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_Edit_PrivatePublicLinkAlert, 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:
|
|
dismissImpl?()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
if state.revokingPeerId != nil {
|
|
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
|
|
}
|
|
|
|
var isGroup = false
|
|
if let peer = peer as? TelegramChannel {
|
|
if case .group = peer.info {
|
|
isGroup = true
|
|
}
|
|
} else if let _ = peer as? TelegramGroup {
|
|
isGroup = true
|
|
}
|
|
|
|
let leftNavigationButton: ItemListNavigationButton?
|
|
switch mode {
|
|
case .initialSetup:
|
|
leftNavigationButton = nil
|
|
case .generic, .privateLink:
|
|
leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
|
|
dismissImpl?()
|
|
})
|
|
}
|
|
|
|
var crossfade: Bool = false
|
|
let hasNamesToRevoke = publicChannelsToRevoke != nil && !publicChannelsToRevoke!.isEmpty
|
|
let hadNamesToRevoke = previousHadNamesToRevoke.swap(hasNamesToRevoke)
|
|
if let peer = view.peers[view.peerId] as? TelegramChannel {
|
|
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 = hadNamesToRevoke != hasNamesToRevoke
|
|
}
|
|
}
|
|
|
|
let title: String
|
|
if case .privateLink = mode {
|
|
title = presentationData.strings.GroupInfo_InviteLink_Title
|
|
} else {
|
|
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
|
|
}
|
|
}
|
|
|
|
let entries = channelVisibilityControllerEntries(presentationData: presentationData, mode: mode, view: view, publicChannelsToRevoke: publicChannelsToRevoke, state: state)
|
|
|
|
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, crossfadeState: crossfade, animateChanges: false)
|
|
|
|
return (controllerState, (listState, arguments))
|
|
} |> afterDisposed {
|
|
actionsDisposable.dispose()
|
|
}
|
|
|
|
let controller = ItemListController(context: context, state: signal)
|
|
controller.willDisappear = { _ in
|
|
dismissTooltipsImpl?()
|
|
}
|
|
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()
|
|
}
|
|
}
|
|
nextImpl = { [weak controller] in
|
|
if let controller = controller {
|
|
if case .initialSetup = mode {
|
|
let selectionController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .channelCreation, options: []))
|
|
(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 {
|
|
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peerId), keepStack: .never, animated: true))
|
|
} else {
|
|
selectionController.displayProgress = true
|
|
let _ = (addChannelMembers(account: context.account, peerId: peerId, memberIds: filteredPeerIds)
|
|
|> deliverOnMainQueue).start(error: { [weak selectionController] _ in
|
|
guard let selectionController = selectionController, let navigationController = selectionController.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
|
|
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peerId), keepStack: .never, animated: true))
|
|
}, completed: { [weak selectionController] in
|
|
guard let selectionController = selectionController, let navigationController = selectionController.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
|
|
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peerId), keepStack: .never, animated: true))
|
|
})
|
|
}
|
|
})
|
|
} else {
|
|
if let navigationController = controller.navigationController as? NavigationController {
|
|
navigationController.replaceAllButRootController(context.sharedContext.makeChatController(context: context, chatLocation: .peer(peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)), animated: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
displayPrivateLinkMenuImpl = { [weak controller] text in
|
|
if let strongController = controller {
|
|
var resultItemNode: ListViewItemNode?
|
|
let _ = strongController.frameForItemNode({ itemNode in
|
|
if let itemNode = itemNode as? ItemListActionItemNode {
|
|
if let tag = itemNode.tag as? ChannelVisibilityEntryTag {
|
|
if tag == .privateLink {
|
|
resultItemNode = itemNode
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
if let resultItemNode = resultItemNode {
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopyLink, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopyLink), action: {
|
|
UIPasteboard.general.string = text
|
|
})])
|
|
strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in
|
|
if let strongController = controller, let resultItemNode = resultItemNode {
|
|
return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds)
|
|
} else {
|
|
return nil
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
private final class InviteLinkContextExtractedContentSource: ContextExtractedContentSource {
|
|
var keepInPlace: Bool
|
|
let ignoreContentTouches: Bool = true
|
|
let blurBackground: Bool
|
|
|
|
private let controller: ViewController
|
|
private let sourceNode: ContextExtractedContentContainingNode
|
|
|
|
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) {
|
|
self.controller = controller
|
|
self.sourceNode = sourceNode
|
|
self.keepInPlace = false
|
|
self.blurBackground = false
|
|
}
|
|
|
|
func takeView() -> ContextControllerTakeViewInfo? {
|
|
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
|
|
}
|
|
|
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
|
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
|
}
|
|
}
|