import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import ItemListUI import PresentationDataUtils import AccountContext import ShareController import UndoUI import InviteLinksUI import TextFormat private final class UsernameSetupControllerArguments { let account: Account let updatePublicLinkText: (String?, String) -> Void let shareLink: () -> Void let activateLink: (String) -> Void let deactivateLink: (String) -> Void let openAuction: (String) -> Void init(account: Account, updatePublicLinkText: @escaping (String?, String) -> Void, shareLink: @escaping () -> Void, activateLink: @escaping (String) -> Void, deactivateLink: @escaping (String) -> Void, openAuction: @escaping (String) -> Void) { self.account = account self.updatePublicLinkText = updatePublicLinkText self.shareLink = shareLink self.activateLink = activateLink self.deactivateLink = deactivateLink self.openAuction = openAuction } } private enum UsernameSetupSection: Int32 { case link case additional } public enum UsernameEntryTag: ItemListItemTag { case username public func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? UsernameEntryTag, self == other { return true } else { return false } } } private enum UsernameSetupEntryId: Hashable { case index(Int32) case username(String) } private enum UsernameSetupEntry: ItemListNodeEntry { case publicLinkHeader(PresentationTheme, String) case editablePublicLink(PresentationTheme, PresentationStrings, String, String?, String, Bool) case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus, String, String) case publicLinkInfo(PresentationTheme, String) case additionalLinkHeader(PresentationTheme, String) case additionalLink(PresentationTheme, TelegramPeerUsername, Int32) case additionalLinkInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { case .publicLinkHeader, .editablePublicLink, .publicLinkStatus, .publicLinkInfo: return UsernameSetupSection.link.rawValue case .additionalLinkHeader, .additionalLink, .additionalLinkInfo: return UsernameSetupSection.additional.rawValue } } var stableId: UsernameSetupEntryId { switch self { case .publicLinkHeader: return .index(0) case .editablePublicLink: return .index(1) case .publicLinkStatus: return .index(2) case .publicLinkInfo: return .index(3) case .additionalLinkHeader: return .index(4) case let .additionalLink(_, username, _): return .username(username.username) case .additionalLinkInfo: return .index(5) } } static func ==(lhs: UsernameSetupEntry, rhs: UsernameSetupEntry) -> Bool { switch lhs { case let .publicLinkHeader(lhsTheme, lhsText): if case let .publicLinkHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .editablePublicLink(lhsTheme, lhsStrings, lhsPrefix, lhsCurrentText, lhsText, lhsEnabled): if case let .editablePublicLink(rhsTheme, rhsStrings, rhsPrefix, rhsCurrentText, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPrefix == rhsPrefix, lhsCurrentText == rhsCurrentText, lhsText == rhsText, lhsEnabled == rhsEnabled { 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, lhsAddressName, lhsStatus, lhsText, lhsUsername): if case let .publicLinkStatus(rhsTheme, rhsAddressName, rhsStatus, rhsText, rhsUsername) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsStatus == rhsStatus, lhsText == rhsText, lhsUsername == rhsUsername { 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 } } } static func <(lhs: UsernameSetupEntry, rhs: UsernameSetupEntry) -> Bool { switch lhs { case .publicLinkHeader: switch rhs { case .publicLinkHeader: return false default: return true } case .editablePublicLink: switch rhs { case .publicLinkHeader, .editablePublicLink: return false default: return true } case .publicLinkStatus: switch rhs { case .publicLinkHeader, .editablePublicLink, .publicLinkStatus: return false default: return true } case .publicLinkInfo: switch rhs { case .publicLinkHeader, .editablePublicLink, .publicLinkStatus, .publicLinkInfo: return false default: return true } case .additionalLinkHeader: switch rhs { case .publicLinkHeader, .editablePublicLink, .publicLinkStatus, .publicLinkInfo, .additionalLinkHeader: return false default: return true } case let .additionalLink(_, _, lhsIndex): switch rhs { case let .additionalLink(_, _, rhsIndex): return lhsIndex < rhsIndex case .publicLinkHeader, .editablePublicLink, .publicLinkStatus, .publicLinkInfo, .additionalLinkHeader: return false default: return true } case .additionalLinkInfo: switch rhs { case .publicLinkHeader, .editablePublicLink, .publicLinkStatus, .publicLinkInfo, .additionalLinkHeader, .additionalLink, .additionalLinkInfo: return false } } } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! UsernameSetupControllerArguments switch self { case let .publicLinkHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .editablePublicLink(theme, _, prefix, currentText, text, enabled): return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: enabled ? prefix : "", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .username, spacing: 10.0, clearType: enabled ? .always : .none, enabled: enabled, tag: UsernameEntryTag.username, sectionId: self.section, textUpdated: { updatedText in arguments.updatePublicLinkText(currentText, updatedText) }, action: { }) case let .publicLinkInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in if case .tap = action { arguments.shareLink() } }) case let .publicLinkStatus(_, _, status, text, 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 .purchaseAvailable: textColor = .generic case .invalid, .taken: textColor = .destructive } 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 .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.isActive { arguments.deactivateLink(link.username) } else { arguments.activateLink(link.username) } } }) case let .additionalLinkInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } private struct UsernameSetupControllerState: Equatable { let editingPublicLinkText: String? let addressNameValidationStatus: AddressNameValidationStatus? let updatingAddressName: Bool init() { self.editingPublicLinkText = nil self.addressNameValidationStatus = nil self.updatingAddressName = false } init(editingPublicLinkText: String?, addressNameValidationStatus: AddressNameValidationStatus?, updatingAddressName: Bool) { self.editingPublicLinkText = editingPublicLinkText self.addressNameValidationStatus = addressNameValidationStatus self.updatingAddressName = updatingAddressName } static func ==(lhs: UsernameSetupControllerState, rhs: UsernameSetupControllerState) -> Bool { if lhs.editingPublicLinkText != rhs.editingPublicLinkText { return false } if lhs.addressNameValidationStatus != rhs.addressNameValidationStatus { return false } if lhs.updatingAddressName != rhs.updatingAddressName { return false } return true } func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?) -> UsernameSetupControllerState { return UsernameSetupControllerState(editingPublicLinkText: editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName) } func withUpdatedAddressNameValidationStatus(_ addressNameValidationStatus: AddressNameValidationStatus?) -> UsernameSetupControllerState { return UsernameSetupControllerState(editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: addressNameValidationStatus, updatingAddressName: self.updatingAddressName) } func withUpdatedUpdatingAddressName(_ updatingAddressName: Bool) -> UsernameSetupControllerState { return UsernameSetupControllerState(editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName) } } private func usernameSetupControllerEntries(presentationData: PresentationData, view: PeerView, state: UsernameSetupControllerState, temporaryOrder: [String]?, mode: UsernameSetupMode) -> [UsernameSetupEntry] { var entries: [UsernameSetupEntry] = [] if let peer = view.peers[view.peerId] as? TelegramUser { let currentUsername: String if let current = state.editingPublicLinkText { currentUsername = current } else { if let username = peer.editableUsername { currentUsername = username } else { currentUsername = "" } } entries.append(.publicLinkHeader(presentationData.theme, presentationData.strings.Username_Username)) entries.append(.editablePublicLink(presentationData.theme, presentationData.strings, presentationData.strings.Username_Title, peer.editableUsername, currentUsername, mode == .account)) if let status = state.addressNameValidationStatus { let statusText: String switch status { case let .invalidFormat(error): switch error { case .startsWithDigit: statusText = presentationData.strings.Username_InvalidStartsWithNumber case .startsWithUnderscore: statusText = presentationData.strings.Username_InvalidStartsWithUnderscore case .endsWithUnderscore: statusText = presentationData.strings.Username_InvalidEndsWithUnderscore case .invalidCharacters: statusText = presentationData.strings.Username_InvalidCharacters case .tooShort: statusText = presentationData.strings.Username_InvalidTooShort } case let .availability(availability): switch availability { case .available: statusText = presentationData.strings.Username_UsernameIsAvailable(currentUsername).string case .invalid: statusText = presentationData.strings.Username_InvalidValue case .taken: statusText = presentationData.strings.Username_InvalidTaken case .purchaseAvailable: var markdownString = presentationData.strings.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)) } statusText = markdownString } case .checking: statusText = presentationData.strings.Username_CheckingUsername } entries.append(.publicLinkStatus(presentationData.theme, currentUsername, status, statusText, currentUsername)) } let otherUsernames = peer.usernames.filter { !$0.flags.contains(.isEditable) } if case .bot = mode { var infoText = presentationData.strings.Username_BotLinkHint if otherUsernames.isEmpty { infoText = presentationData.strings.Username_BotLinkHintExtended } entries.append(.publicLinkInfo(presentationData.theme, infoText)) } else { var infoText = presentationData.strings.Username_Help if otherUsernames.isEmpty { infoText += "\n\n" let hintText = presentationData.strings.Username_LinkHint(currentUsername.replacingOccurrences(of: "[", with: "").replacingOccurrences(of: "]", with: "")).string.replacingOccurrences(of: "]", with: "]()") infoText += hintText } entries.append(.publicLinkInfo(presentationData.theme, infoText)) } if !otherUsernames.isEmpty { entries.append(.additionalLinkHeader(presentationData.theme, presentationData.strings.Username_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 } let text: String switch mode { case .account: text = presentationData.strings.Username_LinksOrderInfo case .bot: text = presentationData.strings.Username_BotLinksOrderInfo } entries.append(.additionalLinkInfo(presentationData.theme, text)) } } return entries } public enum UsernameSetupMode: Equatable { case account case bot(PeerId) } public func usernameSetupController(context: AccountContext, mode: UsernameSetupMode = .account) -> ViewController { let statePromise = ValuePromise(UsernameSetupControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: UsernameSetupControllerState()) let updateState: ((UsernameSetupControllerState) -> UsernameSetupControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var dismissImpl: (() -> Void)? var dismissInputImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? let actionsDisposable = DisposableSet() let checkAddressNameDisposable = MetaDisposable() actionsDisposable.add(checkAddressNameDisposable) let updateAddressNameDisposable = MetaDisposable() actionsDisposable.add(updateAddressNameDisposable) let peerId: PeerId let domain: AddressNameDomain switch mode { case .account: domain = .account peerId = context.account.peerId case let .bot(botPeerId): domain = .bot(botPeerId) peerId = botPeerId } let arguments = UsernameSetupControllerArguments(account: context.account, 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: domain, name: text) |> deliverOnMainQueue).start(next: { result in updateState { state in return state.withUpdatedAddressNameValidationStatus(result) } })) } }, shareLink: { let _ = (context.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in if let user = peer as? TelegramUser, user.botInfo != nil { context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://fragment.com/", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) } else { var currentAddressName: String = peer.addressName ?? "" updateState { state in if let current = state.editingPublicLinkText { currentAddressName = current } return state } if !currentAddressName.isEmpty { dismissInputImpl?() let shareController = ShareController(context: context, subject: .url("https://t.me/\(currentAddressName)")) shareController.actionCompleted = { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) } presentControllerImpl?(shareController, nil) } } }) }, activateLink: { name in dismissInputImpl?() let presentationData = context.sharedContext.currentPresentationData.with { $0 } let alertText: String if case .bot = mode { alertText = presentationData.strings.Username_BotActivateAlertText } else { alertText = presentationData.strings.Username_ActivateAlertText } presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Username_ActivateAlertTitle, text: alertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Username_ActivateAlertShow, action: { let _ = (context.engine.peers.toggleAddressNameActive(domain: domain, name: name, active: true) |> deliverOnMainQueue).start(error: { error in let errorText: String switch error { case .activeLimitReached: if case .bot = mode { errorText = presentationData.strings.Username_BotActiveLimitReachedError } else { errorText = presentationData.strings.Username_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 presentationData = context.sharedContext.currentPresentationData.with { $0 } let alertText: String if case .bot = mode { alertText = presentationData.strings.Username_BotDeactivateAlertText } else { alertText = presentationData.strings.Username_DeactivateAlertText } presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Username_DeactivateAlertTitle, text: alertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Username_DeactivateAlertHide, action: { let _ = context.engine.peers.toggleAddressNameActive(domain: domain, 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 temporaryOrder = Promise<[String]?>(nil) let peerView = context.account.viewTracker.peerView(peerId) |> deliverOnMainQueue let signal = combineLatest( context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, peerView, temporaryOrder.get() ) |> map { presentationData, state, view, temporaryOrder -> (ItemListControllerState, (ItemListNodeState, Any)) in let peer = peerViewMainPeer(view) var rightNavigationButton: ItemListNavigationButton? if let peer = peer as? TelegramUser { var doneEnabled = true if let addressNameValidationStatus = state.addressNameValidationStatus { switch addressNameValidationStatus { case .availability(.available): break default: doneEnabled = false } } rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { var updatedAddressNameValue: String? updateState { state in if state.editingPublicLinkText != peer.addressName { updatedAddressNameValue = state.editingPublicLinkText } if updatedAddressNameValue != nil { return state.withUpdatedUpdatingAddressName(true) } else { return state } } if let updatedAddressNameValue = updatedAddressNameValue { updateAddressNameDisposable.set((context.engine.peers.updateAddressName(domain: domain, name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) |> deliverOnMainQueue).start(error: { _ in updateState { state in return state.withUpdatedUpdatingAddressName(false) } }, completed: { updateState { state in return state.withUpdatedUpdatingAddressName(false) } dismissImpl?() })) } else { dismissImpl?() } }) } let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) let title: String if case .bot = mode { title = presentationData.strings.Username_BotTitle } else { title = presentationData.strings.Username_Title } 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: usernameSetupControllerEntries(presentationData: presentationData, view: view, state: state, temporaryOrder: temporaryOrder, mode: mode), style: .blocks, focusItemTag: mode == .account ? UsernameEntryTag.username : nil, animateChanges: true) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ItemListController(context: context, state: signal) controller.navigationPresentation = .modal controller.enableInteractiveDismiss = true controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [UsernameSetupEntry]) -> 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: [UsernameSetupEntry]) -> 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: domain, names: currentUsernames) |> deliverOnMainQueue).start(completed: { temporaryOrder.set(.single(nil)) }) }) controller.beganInteractiveDragging = { dismissInputImpl?() } dismissImpl = { [weak controller] in controller?.view.endEditing(true) controller?.dismiss() } dismissInputImpl = { [weak controller] in controller?.view.endEditing(true) } presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } return controller }