import Foundation import Display import SwiftSignalKit import Postbox import TelegramCore import LegacyComponents import MtProtoKitDynamic private final class SettingsItemIcons { static let proxy = UIImage(bundleImageName: "Settings/MenuIcons/Proxy")?.precomposed() static let savedMessages = UIImage(bundleImageName: "Settings/MenuIcons/SavedMessages")?.precomposed() static let recentCalls = UIImage(bundleImageName: "Settings/MenuIcons/RecentCalls")?.precomposed() static let stickers = UIImage(bundleImageName: "Settings/MenuIcons/Stickers")?.precomposed() static let notifications = UIImage(bundleImageName: "Settings/MenuIcons/Notifications")?.precomposed() static let security = UIImage(bundleImageName: "Settings/MenuIcons/Security")?.precomposed() static let dataAndStorage = UIImage(bundleImageName: "Settings/MenuIcons/DataAndStorage")?.precomposed() static let appearance = UIImage(bundleImageName: "Settings/MenuIcons/Appearance")?.precomposed() static let language = UIImage(bundleImageName: "Settings/MenuIcons/Language")?.precomposed() static let passport = UIImage(bundleImageName: "Settings/MenuIcons/Passport")?.precomposed() static let watch = UIImage(bundleImageName: "Settings/MenuIcons/Watch")?.precomposed() static let support = UIImage(bundleImageName: "Settings/MenuIcons/Support")?.precomposed() static let faq = UIImage(bundleImageName: "Settings/MenuIcons/Faq")?.precomposed() } private struct SettingsItemArguments { let accountManager: AccountManager let avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext let avatarTapAction: () -> Void let changeProfilePhoto: () -> Void let openUsername: () -> Void let openProxy: () -> Void let openSavedMessages: () -> Void let openRecentCalls: () -> Void let openPrivacyAndSecurity: () -> Void let openDataAndStorage: () -> Void let openStickerPacks: ([ArchivedStickerPackItem]?) -> Void let openNotificationsAndSounds: (NotificationExceptionsList?) -> Void let openThemes: () -> Void let pushController: (ViewController) -> Void let openLanguage: () -> Void let openPassport: () -> Void let openWatch: () -> Void let openSupport: () -> Void let openFaq: () -> Void let openEditing: () -> Void let displayCopyContextMenu: () -> Void let switchToAccount: (AccountRecordId) -> Void let addAccount: () -> Void } private enum SettingsSection: Int32 { case info case accounts case proxy case media case generalSettings case advanced case help } private enum SettingsEntry: ItemListNodeEntry { case userInfo(Account, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer?, CachedPeerData?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?) case setProfilePhoto(PresentationTheme, String) case setUsername(PresentationTheme, String) case account(Int, Account, PresentationTheme, PresentationStrings, Peer, Int32) case addAccount(PresentationTheme, String) case proxy(PresentationTheme, UIImage?, String, String) case savedMessages(PresentationTheme, UIImage?, String) case recentCalls(PresentationTheme, UIImage?, String) case stickers(PresentationTheme, UIImage?, String, String, [ArchivedStickerPackItem]?) case notificationsAndSounds(PresentationTheme, UIImage?, String, NotificationExceptionsList?, Bool) case privacyAndSecurity(PresentationTheme, UIImage?, String) case dataAndStorage(PresentationTheme, UIImage?, String) case themes(PresentationTheme, UIImage?, String) case language(PresentationTheme, UIImage?, String, String) case passport(PresentationTheme, UIImage?, String, String) case watch(PresentationTheme, UIImage?, String, String) case askAQuestion(PresentationTheme, UIImage?, String) case faq(PresentationTheme, UIImage?, String) var section: ItemListSectionId { switch self { case .userInfo, .setProfilePhoto, .setUsername: return SettingsSection.info.rawValue case .account, .addAccount: return SettingsSection.accounts.rawValue case .proxy: return SettingsSection.proxy.rawValue case .savedMessages, .recentCalls, .stickers: return SettingsSection.media.rawValue case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .themes, .language: return SettingsSection.generalSettings.rawValue case .passport, .watch : return SettingsSection.advanced.rawValue case .askAQuestion, .faq: return SettingsSection.help.rawValue } } var stableId: Int32 { switch self { case .userInfo: return 0 case .setProfilePhoto: return 1 case .setUsername: return 2 case let .account(account): return 3 + Int32(account.0) case .addAccount: return 1002 case .proxy: return 1003 case .savedMessages: return 1004 case .recentCalls: return 1005 case .stickers: return 1006 case .notificationsAndSounds: return 1007 case .privacyAndSecurity: return 1008 case .dataAndStorage: return 1009 case .themes: return 1010 case .language: return 1011 case .passport: return 1012 case .watch: return 1013 case .askAQuestion: return 1014 case .faq: return 1015 } } static func ==(lhs: SettingsEntry, rhs: SettingsEntry) -> Bool { switch lhs { case let .userInfo(lhsAccount, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsCachedData, lhsEditingState, lhsUpdatingImage): if case let .userInfo(rhsAccount, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsCachedData, rhsEditingState, rhsUpdatingImage) = rhs { if lhsAccount !== rhsAccount { return false } if lhsTheme !== rhsTheme { return false } if lhsStrings !== rhsStrings { return false } if lhsDateTimeFormat != rhsDateTimeFormat { return false } if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false } } else if (lhsPeer != nil) != (rhsPeer != nil) { return false } if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { if !lhsCachedData.isEqual(to: rhsCachedData) { return false } } else if (lhsCachedData != nil) != (rhsCachedData != nil) { return false } if lhsEditingState != rhsEditingState { return false } if lhsUpdatingImage != rhsUpdatingImage { return false } return true } else { return false } case let .account(lhsIndex, lhsAccount, lhsTheme, lhsStrings, lhsPeer, lhsBadgeCount): if case let .account(rhsIndex, rhsAccount, rhsTheme, rhsStrings, rhsPeer, rhsBadgeCount) = rhs, lhsIndex == rhsIndex, lhsAccount === rhsAccount, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPeer.isEqual(rhsPeer), lhsBadgeCount == rhsBadgeCount { return true } else { return false } case let .addAccount(lhsTheme, lhsText): if case let .addAccount(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .setProfilePhoto(lhsTheme, lhsText): if case let .setProfilePhoto(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .setUsername(lhsTheme, lhsText): if case let .setUsername(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .proxy(lhsTheme, lhsImage, lhsText, lhsValue): if case let .proxy(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .savedMessages(lhsTheme, lhsImage, lhsText): if case let .savedMessages(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } case let .recentCalls(lhsTheme, lhsImage, lhsText): if case let .recentCalls(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } case let .stickers(lhsTheme, lhsImage, lhsText, lhsValue, _): if case let .stickers(rhsTheme, rhsImage, rhsText, rhsValue, _) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .notificationsAndSounds(lhsTheme, lhsImage, lhsText, lhsExceptionsList, lhsWarning): if case let .notificationsAndSounds(rhsTheme, rhsImage, rhsText, rhsExceptionsList, rhsWarning) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsExceptionsList == rhsExceptionsList, lhsWarning == rhsWarning { return true } else { return false } case let .privacyAndSecurity(lhsTheme, lhsImage, lhsText): if case let .privacyAndSecurity(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } case let .dataAndStorage(lhsTheme, lhsImage, lhsText): if case let .dataAndStorage(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } case let .themes(lhsTheme, lhsImage, lhsText): if case let .themes(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } case let .language(lhsTheme, lhsImage, lhsText, lhsValue): if case let .language(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .passport(lhsTheme, lhsImage, lhsText, lhsValue): if case let .passport(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .watch(lhsTheme, lhsImage, lhsText, lhsValue): if case let .watch(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .askAQuestion(lhsTheme, lhsImage, lhsText): if case let .askAQuestion(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } case let .faq(lhsTheme, lhsImage, lhsText): if case let .faq(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true } else { return false } } } static func <(lhs: SettingsEntry, rhs: SettingsEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(_ arguments: SettingsItemArguments) -> ListViewItem { switch self { case let .userInfo(account, theme, strings, dateTimeFormat, peer, cachedData, state, updatingImage): return ItemListAvatarAndNameInfoItem(account: account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .settings, peer: peer, presence: TelegramUserPresence(status: .present(until: Int32.max), lastActivity: 0), cachedData: cachedData, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { _ in }, avatarTapped: { arguments.avatarTapAction() }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingImage, action: { arguments.openEditing() }, longTapAction: { arguments.displayCopyContextMenu() }) case let .setProfilePhoto(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeProfilePhoto() }) case let .setUsername(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openUsername() }) case let .account(_, account, theme, strings, peer, badgeCount): return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .dayFirst, dateSeparator: "."), nameDisplayOrder: .firstLast, account: account, peer: peer, aliasHandling: .standard, presence: nil, text: .none, label: .badge("\(badgeCount)"), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, sectionId: self.section, action: { arguments.switchToAccount(account.id) }, setPeerIdWithRevealedOptions: { lhs, rhs in }, removePeer: { _ in }) case let .addAccount(theme, text): return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, alwaysPlain: false, sectionId: self.section, editing: false, action: { arguments.addAccount() }) case let .proxy(theme, image, text, value): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openProxy() }) case let .savedMessages(theme, image, text): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openSavedMessages() }) case let .recentCalls(theme, image, text): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openRecentCalls() }) case let .stickers(theme, image, text, value, archivedPacks): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, labelStyle: .badge(theme.list.itemAccentColor), sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openStickerPacks(archivedPacks) }) case let .notificationsAndSounds(theme, image, text, exceptionsList, warning): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: warning ? "!" : "", labelStyle: warning ? .badge(theme.list.itemDestructiveColor) : .text, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openNotificationsAndSounds(exceptionsList) }) case let .privacyAndSecurity(theme, image, text): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openPrivacyAndSecurity() }) case let .dataAndStorage(theme, image, text): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openDataAndStorage() }) case let .themes(theme, image, text): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openThemes() }) case let .language(theme, image, text, value): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openLanguage() }) case let .passport(theme, image, text, value): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openPassport() }) case let .watch(theme, image, text, value): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openWatch() }) case let .askAQuestion(theme, image, text): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openSupport() }) case let .faq(theme, image, text): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openFaq() }) } } } private struct SettingsState: Equatable { let updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? init(updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? = nil) { self.updatingAvatar = updatingAvatar } func withUpdatedUpdatingAvatar(_ updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) -> SettingsState { return SettingsState(updatingAvatar: updatingAvatar) } static func ==(lhs: SettingsState, rhs: SettingsState) -> Bool { if lhs.updatingAvatar != rhs.updatingAvatar { return false } return true } } private func settingsEntries(account: Account, presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings, notifyExceptions: NotificationExceptionsList?, notificationsAuthorizationStatus: AccessType, notificationsWarningSuppressed: Bool, unreadTrendingStickerPacks: Int, archivedPacks: [ArchivedStickerPackItem]?, hasPassport: Bool, hasWatchApp: Bool, accountsAndPeers: [(Account, Peer, Int32)]) -> [SettingsEntry] { var entries: [SettingsEntry] = [] if let peer = peerViewMainPeer(view) as? TelegramUser { let userInfoState = ItemListAvatarAndNameInfoItemState(editingName: nil, updatingName: nil) entries.append(.userInfo(account, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, view.cachedData, userInfoState, state.updatingAvatar)) if peer.photo.isEmpty { entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.Settings_SetProfilePhoto)) } if peer.addressName == nil { entries.append(.setUsername(presentationData.theme, presentationData.strings.Settings_SetUsername)) } if !accountsAndPeers.isEmpty { var index = 0 for (peerAccount, peer, badgeCount) in accountsAndPeers { entries.append(.account(index, peerAccount, presentationData.theme, presentationData.strings, peer, badgeCount)) index += 1 } entries.append(.addAccount(presentationData.theme, presentationData.strings.Settings_AddAccount)) } if !proxySettings.servers.isEmpty { let valueString: String if proxySettings.enabled, let activeServer = proxySettings.activeServer { switch activeServer.connection { case .mtp: valueString = presentationData.strings.SocksProxySetup_ProxyTelegram case .socks5: valueString = presentationData.strings.SocksProxySetup_ProxySocks5 } } else { valueString = presentationData.strings.Settings_ProxyDisabled } entries.append(.proxy(presentationData.theme, SettingsItemIcons.proxy, presentationData.strings.Settings_Proxy, valueString)) } entries.append(.savedMessages(presentationData.theme, SettingsItemIcons.savedMessages, presentationData.strings.Settings_SavedMessages)) entries.append(.recentCalls(presentationData.theme, SettingsItemIcons.recentCalls, presentationData.strings.CallSettings_RecentCalls)) entries.append(.stickers(presentationData.theme, SettingsItemIcons.stickers, presentationData.strings.ChatSettings_Stickers, unreadTrendingStickerPacks == 0 ? "" : "\(unreadTrendingStickerPacks)", archivedPacks)) let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed) entries.append(.notificationsAndSounds(presentationData.theme, SettingsItemIcons.notifications, presentationData.strings.Settings_NotificationsAndSounds, notifyExceptions, notificationsWarning)) entries.append(.privacyAndSecurity(presentationData.theme, SettingsItemIcons.security, presentationData.strings.Settings_PrivacySettings)) entries.append(.dataAndStorage(presentationData.theme, SettingsItemIcons.dataAndStorage, presentationData.strings.Settings_ChatSettings)) entries.append(.themes(presentationData.theme, SettingsItemIcons.appearance, presentationData.strings.Settings_Appearance)) let languageName = presentationData.strings.primaryComponent.localizedName entries.append(.language(presentationData.theme, SettingsItemIcons.language, presentationData.strings.Settings_AppLanguage, languageName.isEmpty ? presentationData.strings.Localization_LanguageName : languageName)) if hasPassport { entries.append(.passport(presentationData.theme, SettingsItemIcons.passport, presentationData.strings.Settings_Passport, "")) } if hasWatchApp { entries.append(.watch(presentationData.theme, SettingsItemIcons.watch, presentationData.strings.Settings_AppleWatch, "")) } entries.append(.askAQuestion(presentationData.theme, SettingsItemIcons.support, presentationData.strings.Settings_Support)) entries.append(.faq(presentationData.theme, SettingsItemIcons.faq, presentationData.strings.Settings_FAQ)) } return entries } public protocol SettingsController: class { func updateAccount(account: Account) } private final class SettingsControllerImpl: ItemListController, SettingsController { let accountValue: Promise init(currentAccount: Account, accountValue: Promise, state: Signal<(ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)), NoError>, tabBarItem: Signal?) { self.accountValue = accountValue let presentationData = currentAccount.telegramApplicationContext.currentPresentationData.with { $0 } self.accountValue.set(.single(currentAccount)) let updatedPresentationData = self.accountValue.get() |> mapToSignal { account -> Signal<(theme: PresentationTheme, strings: PresentationStrings), NoError> in return account.telegramApplicationContext.presentationData |> map { ($0.theme, $0.strings) } } super.init(theme: presentationData.theme, strings: presentationData.strings, updatedPresentationData: updatedPresentationData, state: state, tabBarItem: tabBarItem) } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func updateAccount(account: Account) { self.accountValue.set(.single(account)) } } public func settingsController(account currentAccount: Account, accountManager: AccountManager) -> SettingsController & ViewController { let statePromise = ValuePromise(SettingsState(), ignoreRepeated: true) let stateValue = Atomic(value: SettingsState()) let updateState: ((SettingsState) -> SettingsState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? var getNavigationControllerImpl: (() -> NavigationController?)? let actionsDisposable = DisposableSet() let updateAvatarDisposable = MetaDisposable() actionsDisposable.add(updateAvatarDisposable) let supportPeerDisposable = MetaDisposable() actionsDisposable.add(supportPeerDisposable) let hiddenAvatarRepresentationDisposable = MetaDisposable() actionsDisposable.add(hiddenAvatarRepresentationDisposable) let updatePassportDisposable = MetaDisposable() actionsDisposable.add(updatePassportDisposable) let openEditingDisposable = MetaDisposable() actionsDisposable.add(openEditingDisposable) let currentAvatarMixin = Atomic(value: nil) var avatarGalleryTransitionArguments: ((AvatarGalleryEntry) -> GalleryTransitionArguments?)? let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext() var updateHiddenAvatarImpl: (() -> Void)? var changeProfilePhotoImpl: (() -> Void)? var openSavedMessagesImpl: (() -> Void)? var displayCopyContextMenuImpl: ((Peer) -> Void)? let archivedPacks = Promise<[ArchivedStickerPackItem]?>() let accountValue = Promise() let networkArguments = currentAccount.networkArguments let auxiliaryMethods = currentAccount.auxiliaryMethods let rootPath = rootPathForBasePath(currentAccount.telegramApplicationContext.applicationBindings.containerPath) let accountsAndPeers: Signal<[(Account, Peer, Int32)], NoError> = accountManager.accountRecords() |> map { view -> [AccountRecordId] in return view.records.compactMap { record -> AccountRecordId? in if record.attributes.contains(where: { $0 is LoggedOutAccountAttribute }) { return nil } return record.id } } |> distinctUntilChanged |> mapToSignal { recordIds -> Signal<[(Account, Peer, Int32)], NoError> in return accountValue.get() |> mapToSignal { currentAccount -> Signal<[(Account, Peer, Int32)], NoError> in var accounts: [Signal<(Account, Peer, Int32)?, NoError>] = [] func accountWithPeer(_ account: Signal) -> Signal<(Account, Peer, Int32)?, NoError> { return account |> mapToSignal { account -> Signal<(Account, Peer, Int32)?, NoError> in guard let account = account else { return .single(nil) } return combineLatest(account.postbox.peerView(id: account.peerId), renderedTotalUnreadCount(postbox: account.postbox)) |> map { view, totalUnreadCount -> (Peer?, Int32) in return (view.peers[view.peerId], totalUnreadCount.0) } |> distinctUntilChanged { lhs, rhs in return arePeersEqual(lhs.0, rhs.0) && lhs.1 == rhs.1 } |> map { peer, totalUnreadCount -> (Account, Peer, Int32)? in if let peer = peer { return (account, peer, totalUnreadCount) } else { return nil } } } } for id in recordIds { if id == currentAccount.id { continue } else { accounts.append(accountWithPeer(accountWithId(networkArguments: networkArguments, id: id, supplementary: true, rootPath: rootPath, beginWithTestingEnvironment: false, auxiliaryMethods: auxiliaryMethods) |> map { result -> Account? in if case let .authorized(account) = result { return account } else { return nil } })) } } return combineLatest(accounts) |> map { accounts -> [(Account, Peer, Int32)] in return accounts.compactMap({ $0 }) } } } let openFaq: (Promise) -> Void = { resolvedUrl in let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .loading(cancelled: nil)) presentControllerImpl?(controller, nil) let _ = (resolvedUrl.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in controller?.dismiss() openResolvedUrl(resolvedUrl, account: account, navigationController: getNavigationControllerImpl?(), openPeer: { peer, navigation in }, present: { controller, arguments in pushControllerImpl?(controller) }, dismissInput: {}) }) }) } let resolvedUrl = accountValue.get() |> deliverOnMainQueue |> mapToSignal { account -> Signal in var faqUrl = account.telegramApplicationContext.currentPresentationData.with { $0 }.strings.Settings_FAQ_URL if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty { faqUrl = "https://telegram.org/faq#general" } return resolveInstantViewUrl(account: account, url: faqUrl) } var switchToAccountImpl: ((AccountRecordId) -> Void)? let arguments = SettingsItemArguments(accountManager: accountManager, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: { var updating = false updateState { updating = $0.updatingAvatar != nil return $0 } if updating { return } let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in let _ = (account.postbox.loadedPeerWithId(account.peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in if peer.smallProfileImage != nil { let galleryController = AvatarGalleryController(account: account, peer: peer, replaceRootController: { controller, ready in }) hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in avatarAndNameInfoContext.hiddenAvatarRepresentation = entry?.representations.first?.representation updateHiddenAvatarImpl?() })) presentControllerImpl?(galleryController, AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in return avatarGalleryTransitionArguments?(entry) })) } else { changeProfilePhotoImpl?() } }) }) }, changeProfilePhoto: { changeProfilePhotoImpl?() }, openUsername: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in presentControllerImpl?(usernameSetupController(account: account), nil) }) }, openProxy: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in pushControllerImpl?(proxySettingsController(account: account)) }) }, openSavedMessages: { openSavedMessagesImpl?() }, openRecentCalls: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in pushControllerImpl?(CallListController(account: account, mode: .navigation)) }) }, openPrivacyAndSecurity: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in pushControllerImpl?(privacyAndSecurityController(account: account, initialSettings: .single(nil) |> then(requestAccountPrivacySettings(account: account) |> map(Optional.init)))) }) }, openDataAndStorage: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in pushControllerImpl?(dataAndStorageController(account: account)) }) }, openStickerPacks: { archivedPacksValue in let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in pushControllerImpl?(installedStickerPacksController(account: account, mode: .general, archivedPacks: archivedPacksValue, updatedPacks: { packs in archivedPacks.set(.single(packs)) })) }) }, openNotificationsAndSounds: { exceptionsList in let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in pushControllerImpl?(notificationsAndSoundsController(account: account, exceptionsList: exceptionsList)) }) }, openThemes: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in pushControllerImpl?(themeSettingsController(account: account)) }) }, pushController: { controller in pushControllerImpl?(controller) }, openLanguage: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in pushControllerImpl?(LocalizationListController(account: account)) }) }, openPassport: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in let controller = SecureIdAuthController(account: account, mode: .list) presentControllerImpl?(controller, nil) }) }, openWatch: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in let controller = watchSettingsController(account: account) pushControllerImpl?(controller) }) }, openSupport: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in let supportPeer = Promise() supportPeer.set(supportPeerId(account: account)) let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let resolvedUrlPromise = Promise() resolvedUrlPromise.set(resolvedUrl) presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: { openFaq(resolvedUrlPromise) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).start(next: { peerId in if let peerId = peerId { pushControllerImpl?(ChatController(account: account, chatLocation: .peer(peerId))) } })) }) ]), nil) }) }, openFaq: { let resolvedUrlPromise = Promise() resolvedUrlPromise.set(resolvedUrl) openFaq(resolvedUrlPromise) }, openEditing: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in var cancelImpl: (() -> Void)? let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let progressSignal = Signal { subscriber in let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .loading(cancelled: { cancelImpl?() })) presentControllerImpl?(controller, nil) return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() } } } |> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() let peerKey: PostboxViewKey = .peer(peerId: account.peerId, components: []) let cachedDataKey: PostboxViewKey = .cachedPeerData(peerId: account.peerId) let signal = (account.postbox.combinedView(keys: [peerKey, cachedDataKey]) |> mapToSignal { view -> Signal<(TelegramUser, CachedUserData), NoError> in guard let cachedDataView = view.views[cachedDataKey] as? CachedPeerDataView, let cachedData = cachedDataView.cachedPeerData as? CachedUserData else { return .complete() } guard let peerView = view.views[peerKey] as? PeerView, let peer = peerView.peers[account.peerId] as? TelegramUser else { return .complete() } return .single((peer, cachedData)) } |> take(1)) |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } cancelImpl = { openEditingDisposable.set(nil) } openEditingDisposable.set((signal |> deliverOnMainQueue).start(next: { peer, cachedData in pushControllerImpl?(editSettingsController(account: account, currentName: .personName(firstName: peer.firstName ?? "", lastName: peer.lastName ?? ""), currentBioText: cachedData.about ?? "", accountManager: accountManager)) })) }) }, displayCopyContextMenu: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in let _ = (account.postbox.transaction { transaction -> (Peer?) in return transaction.getPeer(account.peerId) } |> deliverOnMainQueue).start(next: { peer in if let peer = peer { displayCopyContextMenuImpl?(peer) } }) }) }, switchToAccount: { id in switchToAccountImpl?(id) }, addAccount: { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in let isTestingEnvironment = account.testingEnvironment let _ = accountManager.transaction({ transaction -> Void in let id = transaction.createRecord([AccountEnvironmentAttribute(environment: isTestingEnvironment ? .test : .production)]) transaction.setCurrentId(id) }).start() }) }) changeProfilePhotoImpl = { let _ = (accountValue.get() |> deliverOnMainQueue |> take(1)).start(next: { account in let _ = (account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in return (transaction.getPeer(account.peerId), currentSearchBotsConfiguration(transaction: transaction)) } |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.statusBar.statusBarStyle = .Ignore let emptyController = LegacyEmptyController(context: legacyController.context)! let navigationController = makeLegacyNavigationController(rootController: emptyController) navigationController.setNavigationBarHidden(true, animated: false) navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) legacyController.bind(controller: navigationController) presentControllerImpl?(legacyController, nil) var hasPhotos = false if let peer = peer, !peer.profileImageRepresentations.isEmpty { hasPhotos = true } let completedImpl: (UIImage) -> Void = { image in if let data = UIImageJPEGRepresentation(image, 0.6) { let resource = LocalFileMediaResource(fileId: arc4random64()) account.postbox.mediaBox.storeResourceData(resource.id, data: data) let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) updateState { $0.withUpdatedUpdatingAvatar(.image(representation, true)) } updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: resource, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) |> deliverOnMainQueue).start(next: { result in switch result { case .complete: updateState { $0.withUpdatedUpdatingAvatar(nil) } case .progress: break } })) } } let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: false, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! let _ = currentAvatarMixin.swap(mixin) mixin.requestSearchController = { assetsController in let controller = WebSearchController(account: account, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: nil, completion: { result in assetsController?.dismiss() completedImpl(result) })) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } mixin.didFinishWithImage = { image in if let image = image { completedImpl(image) } } mixin.didFinishWithDelete = { let _ = currentAvatarMixin.swap(nil) updateState { if let profileImage = peer?.smallProfileImage { return $0.withUpdatedUpdatingAvatar(.image(profileImage, false)) } else { return $0.withUpdatedUpdatingAvatar(.none) } } updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: nil, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) |> deliverOnMainQueue).start(next: { result in switch result { case .complete: updateState { $0.withUpdatedUpdatingAvatar(nil) } case .progress: break } })) } mixin.didDismiss = { [weak legacyController] in let _ = currentAvatarMixin.swap(nil) legacyController?.dismiss() } let menuController = mixin.present() if let menuController = menuController { menuController.customRemoveFromParentViewController = { [weak legacyController] in legacyController?.dismiss() } } }) }) } let peerView = accountValue.get() |> mapToSignal { account -> Signal in return account.viewTracker.peerView(account.peerId) } archivedPacks.set( .single(nil) |> then( accountValue.get() |> mapToSignal { account -> Signal<[ArchivedStickerPackItem]?, NoError> in archivedStickerPacks(account: account) |> map(Optional.init) } ) ) let hasPassport = ValuePromise(false) let updatePassport: () -> Void = { updatePassportDisposable.set(( accountValue.get() |> take(1) |> mapToSignal { account -> Signal in return twoStepAuthData(account.network) |> map { value -> Bool in return value.hasSecretValues } |> `catch` { _ -> Signal in return .single(false) } } |> deliverOnMainQueue).start(next: { value in hasPassport.set(value) })) } updatePassport() let notificationsAuthorizationStatus = Promise(.allowed) if #available(iOSApplicationExtension 10.0, *) { notificationsAuthorizationStatus.set( .single(.allowed) |> then( accountValue.get() |> mapToSignal { account -> Signal in return DeviceAccess.authorizationStatus(account: account, subject: .notifications) } ) ) } let notificationsWarningSuppressed = Promise(true) if #available(iOSApplicationExtension 10.0, *) { let warningKey = PostboxViewKey.noticeEntry(ApplicationSpecificNotice.notificationsPermissionWarningKey()) notificationsWarningSuppressed.set( .single(true) |> then( accountValue.get() |> mapToSignal { account -> Signal in return account.postbox.combinedView(keys: [warningKey]) |> map { combined -> Bool in let timestamp = (combined.views[warningKey] as? NoticeEntryView)?.value.flatMap({ ApplicationSpecificNotice.getTimestampValue($0) }) if let timestamp = timestamp, timestamp > 0 { return true } else { return false } } } ) ) } let notifyExceptions = Promise(nil) let updateNotifyExceptions: () -> Void = { notifyExceptions.set( accountValue.get() |> take(1) |> mapToSignal { account -> Signal in return notificationExceptionsList(network: account.network) |> map(Optional.init) } ) } let hasWatchApp = Promise(false) hasWatchApp.set( accountValue.get() |> mapToSignal { account -> Signal in if let context = account.applicationContext as? TelegramApplicationContext, let watchManager = context.watchManager { return watchManager.watchAppInstalled } else { return .single(false) } } ) let updatedPresentationData = accountValue.get() |> mapToSignal { account -> Signal in return account.telegramApplicationContext.presentationData } let proxyPreferences = accountValue.get() |> mapToSignal { account in return account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings]) } let featuredStickerPacks = accountValue.get() |> mapToSignal { account in return account.viewTracker.featuredStickerPacks() } let signal = combineLatest(queue: Queue.mainQueue(), accountValue.get(), updatedPresentationData, statePromise.get(), peerView, combineLatest(queue: Queue.mainQueue(), proxyPreferences, notifyExceptions.get(), notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get()), combineLatest(featuredStickerPacks, archivedPacks.get()), combineLatest(hasPassport.get(), hasWatchApp.get()), accountsAndPeers) |> map { account, presentationData, state, view, preferencesAndExceptions, featuredAndArchived, hasPassportAndWatch, accountsAndPeers -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in let proxySettings: ProxySettings = preferencesAndExceptions.0.values[PreferencesKeys.proxySettings] as? ProxySettings ?? ProxySettings.defaultSettings let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { arguments.openEditing() }) let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Settings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) var unreadTrendingStickerPacks = 0 for item in featuredAndArchived.0 { if item.unread { unreadTrendingStickerPacks += 1 } } let (hasPassport, hasWatchApp) = hasPassportAndWatch let listState = ItemListNodeState(entries: settingsEntries(account: account, presentationData: presentationData, state: state, view: view, proxySettings: proxySettings, notifyExceptions: preferencesAndExceptions.1, notificationsAuthorizationStatus: preferencesAndExceptions.2, notificationsWarningSuppressed: preferencesAndExceptions.3, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedPacks: featuredAndArchived.1, hasPassport: hasPassport, hasWatchApp: hasWatchApp, accountsAndPeers: accountsAndPeers), style: .blocks) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let icon: UIImage? if (useSpecialTabBarIcons()) { icon = UIImage(bundleImageName: "Chat List/Tabs/NY/IconSettings") } else { icon = UIImage(bundleImageName: "Chat List/Tabs/IconSettings") } let controller = SettingsControllerImpl(currentAccount: currentAccount, accountValue: accountValue, state: signal, tabBarItem: combineLatest(updatedPresentationData, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get()) |> map { presentationData, notificationsAuthorizationStatus, notificationsWarningSuppressed in let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed) return ItemListControllerTabBarItem(title: presentationData.strings.Settings_Title, image: icon, selectedImage: icon, badgeValue: notificationsWarning ? "!" : nil) }) pushControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true) } presentControllerImpl = { [weak controller] value, arguments in controller?.present(value, in: .window(.root), with: arguments ?? ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } getNavigationControllerImpl = { [weak controller] in return (controller?.navigationController as? NavigationController) } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { var result: ((ASDisplayNode, () -> UIView?), CGRect)? controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { result = itemNode.avatarTransitionNode() } } if let (node, _) = result { return GalleryTransitionArguments(transitionNode: node, addToTransitionSurface: { _ in }) } } return nil } updateHiddenAvatarImpl = { [weak controller] in if let controller = controller { controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { itemNode.updateAvatarHidden() } } } } openSavedMessagesImpl = { [weak controller] in let _ = (accountValue.get() |> take(1) |> deliverOnMainQueue).start(next: { account in if let controller = controller, let navigationController = controller.navigationController as? NavigationController { navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(account.peerId)) } }) } controller.tabBarItemDebugTapAction = { let _ = (accountValue.get() |> take(1) |> deliverOnMainQueue).start(next: { account in pushControllerImpl?(debugController(account: account, accountManager: accountManager)) }) } displayCopyContextMenuImpl = { [weak controller] peer in let _ = (accountValue.get() |> take(1) |> deliverOnMainQueue).start(next: { account in if let strongController = controller { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } var resultItemNode: ListViewItemNode? let _ = strongController.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { resultItemNode = itemNode return true } return false }) if let resultItemNode = resultItemNode, let user = peer as? TelegramUser { var actions: [ContextMenuAction] = [] if let phone = user.phone, !phone.isEmpty { actions.append(ContextMenuAction(content: .text(presentationData.strings.Settings_CopyPhoneNumber), action: { UIPasteboard.general.string = formatPhoneNumber(phone) })) } if let username = user.username, !username.isEmpty { actions.append(ContextMenuAction(content: .text(presentationData.strings.Settings_CopyUsername), action: { UIPasteboard.general.string = username })) } let contextMenuController = ContextMenuController(actions: actions) 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 } })) } } }) } switchToAccountImpl = { [weak controller] id in AccountStore.switchToAccount(id: id, fromSettingsController: controller) } controller.didAppear = { _ in updatePassport() updateNotifyExceptions() } return controller }