diff --git a/submodules/SettingsUI/Sources/SettingsController.swift b/submodules/SettingsUI/Sources/SettingsController.swift index 7474df809b..d790a2a138 100644 --- a/submodules/SettingsUI/Sources/SettingsController.swift +++ b/submodules/SettingsUI/Sources/SettingsController.swift @@ -2,2007 +2,15 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import SwiftSignalKit import Postbox import TelegramCore import SyncCore -import LegacyComponents -import MtProtoKit -import TelegramPresentationData -import TelegramUIPreferences -import DeviceAccess -import ItemListUI -import PresentationDataUtils import AccountContext -import OverlayStatusController -import AvatarNode -import AlertUI -import TelegramNotices -import GalleryUI -import LegacyUI -import PassportUI -import SearchUI -import ItemListPeerItem -import CallListUI -import ChatListUI -import ItemListAvatarAndNameInfoItem -import ItemListPeerActionItem -import WebSearchUI -import PeerAvatarGalleryUI -import MapResourceToAvatarSizes -import AppBundle -import ContextUI -#if ENABLE_WALLET -import WalletUI -#endif -import PhoneNumberFormat -import AccountUtils -import AuthTransferUI -import Emoji -import LegacyMediaPickerUI - -private let avatarFont = avatarPlaceholderFont(size: 13.0) - -private final class ContextControllerContentSourceImpl: ContextControllerContentSource { - let controller: ViewController - weak var sourceNode: ASDisplayNode? - - let navigationController: NavigationController? = nil - - let passthroughTouches: Bool = false - - init(controller: ViewController, sourceNode: ASDisplayNode?) { - self.controller = controller - self.sourceNode = sourceNode - } - - func transitionInfo() -> ContextControllerTakeControllerInfo? { - let sourceNode = self.sourceNode - return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in - if let sourceNode = sourceNode { - return (sourceNode, sourceNode.bounds) - } else { - return nil - } - }) - } - - func animatedIn() { - } -} - -private indirect enum SettingsEntryTag: Equatable, ItemListItemTag { - case account(AccountRecordId) - - func isEqual(to other: ItemListItemTag) -> Bool { - if let other = other as? SettingsEntryTag { - return self == other - } else { - return false - } - } -} - -private final class SettingsItemArguments { - let sharedContext: SharedAccountContext - let avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext - - let avatarTapAction: () -> Void - - let changeProfilePhoto: () -> Void - let openUsername: () -> Void - let openProxy: () -> Void - let openSavedMessages: () -> Void - let openRecentCalls: () -> Void - let openPrivacyAndSecurity: (AccountPrivacySettings?) -> 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 openWallet: () -> Void - let openWatch: () -> Void - let openSupport: () -> Void - let openFaq: (String?) -> Void - let openEditing: () -> Void - let displayCopyContextMenu: () -> Void - let switchToAccount: (AccountRecordId) -> Void - let addAccount: () -> Void - let setAccountIdWithRevealedOptions: (AccountRecordId?, AccountRecordId?) -> Void - let removeAccount: (AccountRecordId) -> Void - let keepPhone: () -> Void - let openPhoneNumberChange: () -> Void - let accountContextAction: (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void - let openDevices: () -> Void - let openFilters: () -> Void - - init( - sharedContext: SharedAccountContext, - avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, - - avatarTapAction: @escaping () -> Void, - - changeProfilePhoto: @escaping () -> Void, - openUsername: @escaping () -> Void, - openProxy: @escaping () -> Void, - openSavedMessages: @escaping () -> Void, - openRecentCalls: @escaping () -> Void, - openPrivacyAndSecurity: @escaping (AccountPrivacySettings?) -> Void, - openDataAndStorage: @escaping () -> Void, - openStickerPacks: @escaping ([ArchivedStickerPackItem]?) -> Void, - openNotificationsAndSounds: @escaping (NotificationExceptionsList?) -> Void, - openThemes: @escaping () -> Void, - pushController: @escaping (ViewController) -> Void, - openLanguage: @escaping () -> Void, - openPassport: @escaping () -> Void, - openWallet: @escaping () -> Void, - openWatch: @escaping () -> Void, - openSupport: @escaping () -> Void, - openFaq: @escaping (String?) -> Void, - openEditing: @escaping () -> Void, - displayCopyContextMenu: @escaping () -> Void, - switchToAccount: @escaping (AccountRecordId) -> Void, - addAccount: @escaping () -> Void, - setAccountIdWithRevealedOptions: @escaping (AccountRecordId?, AccountRecordId?) -> Void, - removeAccount: @escaping (AccountRecordId) -> Void, - keepPhone: @escaping () -> Void, - openPhoneNumberChange: @escaping () -> Void, - accountContextAction: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void, - openDevices: @escaping () -> Void, - openFilters: @escaping () -> Void - ) { - self.sharedContext = sharedContext - self.avatarAndNameInfoContext = avatarAndNameInfoContext - - self.avatarTapAction = avatarTapAction - - self.changeProfilePhoto = changeProfilePhoto - self.openUsername = openUsername - self.openProxy = openProxy - self.openSavedMessages = openSavedMessages - self.openRecentCalls = openRecentCalls - self.openPrivacyAndSecurity = openPrivacyAndSecurity - self.openDataAndStorage = openDataAndStorage - self.openStickerPacks = openStickerPacks - self.openNotificationsAndSounds = openNotificationsAndSounds - self.openThemes = openThemes - self.pushController = pushController - self.openLanguage = openLanguage - self.openPassport = openPassport - self.openWallet = openWallet - self.openWatch = openWatch - self.openSupport = openSupport - self.openFaq = openFaq - self.openEditing = openEditing - self.displayCopyContextMenu = displayCopyContextMenu - self.switchToAccount = switchToAccount - self.addAccount = addAccount - self.setAccountIdWithRevealedOptions = setAccountIdWithRevealedOptions - self.removeAccount = removeAccount - self.keepPhone = keepPhone - self.openPhoneNumberChange = openPhoneNumberChange - self.accountContextAction = accountContextAction - self.openDevices = openDevices - self.openFilters = openFilters - } -} - -private enum SettingsSection: Int32 { - case info - case phone - case accounts - case proxy - case media - case generalSettings - case advanced - case help -} - -private indirect enum SettingsEntry: ItemListNodeEntry { - case userInfo(Account, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer?, CachedPeerData?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?) - case setProfilePhoto(PresentationTheme, String) - case setUsername(PresentationTheme, String) - - case phoneInfo(PresentationTheme, String, String) - case keepPhone(PresentationTheme, String) - case changePhone(PresentationTheme, String) - - case account(Int, Account, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, Int32, Bool) - case addAccount(PresentationTheme, String) - - case proxy(PresentationTheme, UIImage?, String, String) - - case devices(PresentationTheme, UIImage?, String, String) - - case filters(PresentationTheme, UIImage?, String, String) - - case savedMessages(PresentationTheme, UIImage?, String) - case recentCalls(PresentationTheme, UIImage?, String) - case stickers(PresentationTheme, UIImage?, String, String, [ArchivedStickerPackItem]?) - case contentStickers(PresentationTheme, UIImage?, String, String, [ArchivedStickerPackItem]?) - - case notificationsAndSounds(PresentationTheme, UIImage?, String, NotificationExceptionsList?, Bool) - case privacyAndSecurity(PresentationTheme, UIImage?, String, AccountPrivacySettings?) - case dataAndStorage(PresentationTheme, UIImage?, String) - case themes(PresentationTheme, UIImage?, String) - case language(PresentationTheme, UIImage?, String, String) - case passport(PresentationTheme, UIImage?, String, String) - #if ENABLE_WALLET - case wallet(PresentationTheme, UIImage?, String, String) - #endif - 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 .phoneInfo, .keepPhone, .changePhone: - return SettingsSection.phone.rawValue - case .account, .addAccount: - return SettingsSection.accounts.rawValue - case .proxy: - return SettingsSection.proxy.rawValue - case .devices, .filters: - return SettingsSection.media.rawValue - case .savedMessages, .recentCalls, .stickers: - return SettingsSection.media.rawValue - case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .themes, .language, .contentStickers: - return SettingsSection.generalSettings.rawValue - case .passport, .watch: - return SettingsSection.advanced.rawValue - #if ENABLE_WALLET - case .wallet: - return SettingsSection.advanced.rawValue - #endif - 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 .phoneInfo: - return 3 - case .keepPhone: - return 4 - case .changePhone: - return 5 - case let .account(account): - return 6 + Int32(account.0) - case .addAccount: - return 1002 - case .proxy: - return 1003 - case .savedMessages: - return 1004 - case .recentCalls: - return 1005 - case .stickers: - return 1006 - case .devices: - return 1007 - case .filters: - return 1008 - case .notificationsAndSounds: - return 1009 - case .privacyAndSecurity: - return 1010 - case .dataAndStorage: - return 1011 - case .themes: - return 1012 - case .language: - return 1013 - case .contentStickers: - return 1014 - #if ENABLE_WALLET - case .wallet: - return 1015 - #endif - case .passport: - return 1016 - case .watch: - return 1017 - case .askAQuestion: - return 1018 - case .faq: - return 1019 - } - } - - 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, lhsDateTimeFormat, lhsPeer, lhsBadgeCount, lhsRevealed): - if case let .account(rhsIndex, rhsAccount, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsBadgeCount, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsAccount === rhsAccount, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer.isEqual(rhsPeer), lhsBadgeCount == rhsBadgeCount, lhsRevealed == rhsRevealed { - return true - } else { - return false - } - case let .phoneInfo(lhsTheme, lhsTitle, lhsText): - if case let .phoneInfo(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText { - return true - } else { - return false - } - case let .keepPhone(lhsTheme, lhsText): - if case let .keepPhone(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .changePhone(lhsTheme, lhsText): - if case let .changePhone(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - 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 .devices(lhsTheme, lhsImage, lhsText, lhsValue): - if case let .devices(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .filters(lhsTheme, lhsImage, lhsText, lhsValue): - if case let .filters(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 .contentStickers(lhsTheme, lhsImage, lhsText, lhsValue, _): - if case let .contentStickers(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, lhsSettings): - if case let .privacyAndSecurity(rhsTheme, rhsImage, rhsText, rhsSettings) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsSettings == rhsSettings { - 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 - } - #if ENABLE_WALLET - case let .wallet(lhsTheme, lhsImage, lhsText, lhsValue): - if case let .wallet(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - #endif - 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(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { - let arguments = arguments as! SettingsItemArguments - switch self { - case let .userInfo(account, theme, strings, dateTimeFormat, peer, cachedData, state, updatingImage): - return ItemListAvatarAndNameInfoItem(accountContext: arguments.sharedContext.makeTempAccountContext(account: account), presentationData: presentationData, 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, withExtendedBottomInset: false), editingNameUpdated: { _ in - }, avatarTapped: { - arguments.avatarTapAction() - }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingImage, action: { - arguments.openEditing() - }, longTapAction: { - arguments.displayCopyContextMenu() - }) - case let .setProfilePhoto(theme, text): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.changeProfilePhoto() - }) - case let .setUsername(theme, text): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openUsername() - }) - case let .phoneInfo(theme, title, text): - return ItemListInfoItem(presentationData: presentationData, title: title, text: .markdown(text), style: .blocks, sectionId: self.section, linkAction: { action in - if case .tap = action { - arguments.openFaq("q-i-have-a-new-phone-number-what-do-i-do") - } - }, closeAction: nil) - case let .keepPhone(theme, text): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.keepPhone() - }) - case let .changePhone(theme, text): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openPhoneNumberChange() - }) - case let .account(_, account, theme, strings, dateTimeFormat, peer, badgeCount, revealed): - var label: ItemListPeerItemLabel = .none - if badgeCount > 0 { - label = .badge(compactNumericCountString(Int(badgeCount), decimalSeparator: dateTimeFormat.decimalSeparator)) - } - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.sharedContext.makeTempAccountContext(account: account), peer: peer, height: .generic, aliasHandling: .standard, nameStyle: .plain, presence: nil, text: .none, label: label, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { - arguments.switchToAccount(account.id) - }, setPeerIdWithRevealedOptions: { lhs, rhs in - var lhsAccountId: AccountRecordId? - if lhs == peer.id { - lhsAccountId = account.id - } - var rhsAccountId: AccountRecordId? - if rhs == peer.id { - rhsAccountId = account.id - } - arguments.setAccountIdWithRevealedOptions(lhsAccountId, rhsAccountId) - }, removePeer: { _ in - arguments.removeAccount(account.id) - }, contextAction: { node, gesture in - arguments.accountContextAction(account.id, node, gesture) - }, tag: SettingsEntryTag.account(account.id)) - case let .addAccount(theme, text): - return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: { - arguments.addAccount() - }) - case let .proxy(theme, image, text, value): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openProxy() - }) - case let .devices(theme, image, text, value): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openDevices() - }) - case let .filters(theme, image, text, value): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openFilters() - }) - case let .savedMessages(theme, image, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openSavedMessages() - }, clearHighlightAutomatically: false) - case let .recentCalls(theme, image, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openRecentCalls() - }, clearHighlightAutomatically: false) - case let .stickers(theme, image, text, value, archivedPacks): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, labelStyle: .badge(theme.list.itemAccentColor), sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openStickerPacks(archivedPacks) - }, clearHighlightAutomatically: false) - case let .contentStickers(theme, image, text, value, archivedPacks): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, labelStyle: .badge(theme.list.itemAccentColor), sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openStickerPacks(archivedPacks) - }, clearHighlightAutomatically: false) - case let .notificationsAndSounds(theme, image, text, exceptionsList, warning): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: warning ? "!" : "", labelStyle: warning ? .badge(theme.list.itemDestructiveColor) : .text, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openNotificationsAndSounds(exceptionsList) - }, clearHighlightAutomatically: false) - case let .privacyAndSecurity(theme, image, text, privacySettings): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openPrivacyAndSecurity(privacySettings) - }, clearHighlightAutomatically: false) - case let .dataAndStorage(theme, image, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openDataAndStorage() - }, clearHighlightAutomatically: false) - case let .themes(theme, image, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openThemes() - }, clearHighlightAutomatically: false) - case let .language(theme, image, text, value): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openLanguage() - }, clearHighlightAutomatically: false) - case let .passport(theme, image, text, value): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openPassport() - }) - #if ENABLE_WALLET - case let .wallet(theme, image, text, value): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openWallet() - }) - #endif - case let .watch(theme, image, text, value): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openWatch() - }, clearHighlightAutomatically: false) - case let .askAQuestion(theme, image, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openSupport() - }) - case let .faq(theme, image, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { - arguments.openFaq(nil) - }, clearHighlightAutomatically: false) - } - } -} - -private struct SettingsState: Equatable { - var updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? - var accountIdWithRevealedOptions: AccountRecordId? - var isSearching: Bool -} - -private func settingsEntries(account: Account, presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings, notifyExceptions: NotificationExceptionsList?, notificationsAuthorizationStatus: AccessType, notificationsWarningSuppressed: Bool, unreadTrendingStickerPacks: Int, archivedPacks: [ArchivedStickerPackItem]?, privacySettings: AccountPrivacySettings?, hasWallet: Bool, hasPassport: Bool, hasWatchApp: Bool, accountsAndPeers: [(Account, Peer, Int32)], inAppNotificationSettings: InAppNotificationSettings, experimentalUISettings: ExperimentalUISettings, displayPhoneNumberConfirmation: Bool, otherSessionCount: Int, enableQRLogin: Bool, enableFilters: Bool) -> [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_SetProfilePhotoOrVideo)) - } - if peer.addressName == nil { - entries.append(.setUsername(presentationData.theme, presentationData.strings.Settings_SetUsername)) - } - - if displayPhoneNumberConfirmation { - let phoneNumber = formatPhoneNumber(peer.phone ?? "") - entries.append(.phoneInfo(presentationData.theme, presentationData.strings.Settings_CheckPhoneNumberTitle(phoneNumber).0, presentationData.strings.Settings_CheckPhoneNumberText)) - entries.append(.keepPhone(presentationData.theme, presentationData.strings.Settings_KeepPhoneNumber(phoneNumber).0)) - entries.append(.changePhone(presentationData.theme, presentationData.strings.Settings_ChangePhoneNumber)) - } - - if !accountsAndPeers.isEmpty { - var index = 0 - for (peerAccount, peer, badgeCount) in accountsAndPeers { - entries.append(.account(index, peerAccount, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, inAppNotificationSettings.displayNotificationsFromAllAccounts ? badgeCount : 0, state.accountIdWithRevealedOptions == peerAccount.id)) - index += 1 - } - if accountsAndPeers.count + 1 < maximumNumberOfAccounts { - 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, PresentationResourcesSettings.proxy, presentationData.strings.Settings_Proxy, valueString)) - } - - entries.append(.savedMessages(presentationData.theme, PresentationResourcesSettings.savedMessages, presentationData.strings.Settings_SavedMessages)) - entries.append(.recentCalls(presentationData.theme, PresentationResourcesSettings.recentCalls, presentationData.strings.CallSettings_RecentCalls)) - if enableQRLogin { - entries.append(.devices(presentationData.theme, UIImage(bundleImageName: "Settings/MenuIcons/Sessions")?.precomposed(), presentationData.strings.Settings_Devices, otherSessionCount == 0 ? presentationData.strings.Settings_AddDevice : "\(otherSessionCount + 1)")) - } else { - entries.append(.devices(presentationData.theme, UIImage(bundleImageName: "Settings/MenuIcons/Sessions")?.precomposed(), presentationData.strings.Settings_Devices, otherSessionCount == 0 ? "" : "\(otherSessionCount + 1)")) - } - if enableFilters { - entries.append(.filters(presentationData.theme, UIImage(bundleImageName: "Settings/MenuIcons/ChatListFilters")?.precomposed(), presentationData.strings.Settings_ChatFolders, "")) - } - - let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed) - entries.append(.notificationsAndSounds(presentationData.theme, PresentationResourcesSettings.notifications, presentationData.strings.Settings_NotificationsAndSounds, notifyExceptions, notificationsWarning)) - entries.append(.privacyAndSecurity(presentationData.theme, PresentationResourcesSettings.security, presentationData.strings.Settings_PrivacySettings, privacySettings)) - entries.append(.dataAndStorage(presentationData.theme, PresentationResourcesSettings.dataAndStorage, presentationData.strings.Settings_ChatSettings)) - entries.append(.themes(presentationData.theme, PresentationResourcesSettings.appearance, presentationData.strings.Settings_Appearance)) - let languageName = presentationData.strings.primaryComponent.localizedName - entries.append(.language(presentationData.theme, PresentationResourcesSettings.language, presentationData.strings.Settings_AppLanguage, languageName.isEmpty ? presentationData.strings.Localization_LanguageName : languageName)) - entries.append(.contentStickers(presentationData.theme, PresentationResourcesSettings.stickers, presentationData.strings.ChatSettings_Stickers, unreadTrendingStickerPacks == 0 ? "" : "\(unreadTrendingStickerPacks)", archivedPacks)) - - #if ENABLE_WALLET - if hasWallet { - entries.append(.wallet(presentationData.theme, PresentationResourcesSettings.wallet, "Gram Wallet", "")) - } - #endif - if hasPassport { - entries.append(.passport(presentationData.theme, PresentationResourcesSettings.passport, presentationData.strings.Settings_Passport, "")) - } - - if hasWatchApp { - entries.append(.watch(presentationData.theme, PresentationResourcesSettings.watch, presentationData.strings.Settings_AppleWatch, "")) - } - - entries.append(.askAQuestion(presentationData.theme, PresentationResourcesSettings.support, presentationData.strings.Settings_Support)) - entries.append(.faq(presentationData.theme, PresentationResourcesSettings.faq, presentationData.strings.Settings_FAQ)) - } - - return entries -} public protocol SettingsController: class { func updateContext(context: AccountContext) } -private final class SettingsControllerImpl: ItemListController, SettingsController { - let sharedContext: SharedAccountContext - let contextValue: Promise - var accountsAndPeersValue: ((Account, Peer)?, [(Account, Peer, Int32)])? - var accountsAndPeersDisposable: Disposable? - - var switchToAccount: ((AccountRecordId) -> Void)? - var addAccount: (() -> Void)? - - override var navigationBarRequiresEntireLayoutUpdate: Bool { - return false - } - - init(currentContext: AccountContext, contextValue: Promise, state: Signal<(ItemListControllerState, (ItemListNodeState, Any)), NoError>, tabBarItem: Signal?, accountsAndPeers: Signal<((Account, Peer)?, [(Account, Peer, Int32)]), NoError>) { - self.sharedContext = currentContext.sharedContext - self.contextValue = contextValue - let presentationData = currentContext.sharedContext.currentPresentationData.with { $0 } - - self.contextValue.set(.single(currentContext)) - - let updatedPresentationData = self.contextValue.get() - |> mapToSignal { context -> Signal in - return context.sharedContext.presentationData - } - - super.init(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: updatedPresentationData |> map(ItemListPresentationData.init(_:)), state: state, tabBarItem: tabBarItem) - - self.tabBarItemContextActionType = .always - - self.accountsAndPeersDisposable = (accountsAndPeers - |> deliverOnMainQueue).start(next: { [weak self] value in - self?.accountsAndPeersValue = value - }) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.accountsAndPeersDisposable?.dispose() - } - - func updateContext(context: AccountContext) { - //self.contextValue.set(.single(context)) - } - - override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) { - guard let (maybePrimary, other) = self.accountsAndPeersValue, let primary = maybePrimary else { - return - } - - let presentationData = self.sharedContext.currentPresentationData.with { $0 } - let strings = presentationData.strings - - var items: [ContextMenuItem] = [] - if other.count + 1 < maximumNumberOfAccounts { - items.append(.action(ContextMenuActionItem(text: strings.Settings_AddAccount, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - guard let strongSelf = self else { - return - } - strongSelf.addAccount?() - f(.dismissWithoutContent) - }))) - } - - func accountIconSignal(account: Account, peer: Peer, size: CGSize) -> Signal { - let iconSignal: Signal - if let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.first, displayDimensions: size, inset: 0.0, emptyColor: nil, synchronousLoad: false) { - iconSignal = signal - |> map { imageVersions -> UIImage? in - return imageVersions?.0 - } - } else { - let peerId = peer.id - var displayLetters = peer.displayLetters - if displayLetters.count == 2 && displayLetters[0].isSingleEmoji && displayLetters[1].isSingleEmoji { - displayLetters = [displayLetters[0]] - } - iconSignal = Signal { subscriber in - let image = generateImage(size, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - drawPeerAvatarLetters(context: context, size: CGSize(width: size.width, height: size.height), font: avatarFont, letters: displayLetters, peerId: peerId) - })?.withRenderingMode(.alwaysOriginal) - - subscriber.putNext(image) - subscriber.putCompletion() - return EmptyDisposable - } - } - return iconSignal - } - - let avatarSize = CGSize(width: 28.0, height: 28.0) - - items.append(.action(ContextMenuActionItem(text: primary.1.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: accountIconSignal(account: primary.0, peer: primary.1, size: avatarSize)), action: { _, f in - f(.default) - }))) - - if !other.isEmpty { - items.append(.separator) - } - - for account in other { - let id = account.0.id - items.append(.action(ContextMenuActionItem(text: account.1.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), badge: account.2 != 0 ? ContextMenuActionBadge(value: "\(account.2)", color: .accent) : nil, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: accountIconSignal(account: account.0, peer: account.1, size: avatarSize)), action: { [weak self] _, f in - guard let strongSelf = self else { - return - } - strongSelf.switchToAccount?(id) - f(.dismissWithoutContent) - }))) - } - - let controller = ContextController(account: primary.0, presentationData: presentationData, source: .extracted(SettingsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture) - self.sharedContext.mainWindow?.presentInGlobalOverlay(controller) - } -} - -private final class SettingsTabBarContextExtractedContentSource: ContextExtractedContentSource { - let keepInPlace: Bool = true - let ignoreContentTouches: Bool = true - - private let controller: ViewController - private let sourceNode: ContextExtractedContentContainingNode - - init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) { - self.controller = controller - self.sourceNode = sourceNode - } - - func takeView() -> ContextControllerTakeViewInfo? { - return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds) - } - - func putBack() -> ContextControllerPutBackViewInfo? { - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) - } -} - -public func settingsController(context: AccountContext, accountManager: AccountManager, enableDebugActions: Bool) -> SettingsController & ViewController { - let initialState = SettingsState(updatingAvatar: nil, accountIdWithRevealedOptions: nil, isSearching: false) - let statePromise = ValuePromise(initialState, ignoreRepeated: true) - let stateValue = Atomic(value: initialState) - let updateState: ((SettingsState) -> SettingsState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } - - var pushControllerImpl: ((ViewController) -> Void)? - var presentControllerImpl: ((ViewController, Any?) -> Void)? - var presentInGlobalOverlayImpl: ((ViewController, Any?) -> Void)? - var dismissInputImpl: (() -> Void)? - var setDisplayNavigationBarImpl: ((Bool) -> 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 contextValue = Promise() - let accountsAndPeers = Promise<((Account, Peer)?, [(Account, Peer, Int32)])>() - accountsAndPeers.set(activeAccountsAndPeers(context: context)) - - let privacySettings = Promise(nil) - - let enableQRLogin = Promise() - let enableFilters = Promise() - - let openFaq: (Promise, String?) -> Void = { resolvedUrl, customAnchor in - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) - presentControllerImpl?(controller, nil) - let _ = (resolvedUrl.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in - controller?.dismiss() - - var resolvedUrl = resolvedUrl - if case let .instantView(webPage, _) = resolvedUrl, let customAnchor = customAnchor { - resolvedUrl = .instantView(webPage, customAnchor) - } - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: getNavigationControllerImpl?(), openPeer: { peer, navigation in - }, sendFile: nil, sendSticker: nil, present: { controller, arguments in - pushControllerImpl?(controller) - }, dismissInput: {}, contentContext: nil) - }) - }) - } - - let resolvedUrl = contextValue.get() - |> deliverOnMainQueue - |> mapToSignal { context -> Signal in - return cachedFaqInstantPage(context: context) - } - - var removeAccountImpl: ((AccountRecordId) -> Void)? - var switchToAccountImpl: ((AccountRecordId) -> Void)? - - let displayPhoneNumberConfirmation = ValuePromise(false) - - let activeSessionsContextAndCountSignal = contextValue.get() - |> deliverOnMainQueue - |> mapToSignal { context -> Signal<(ActiveSessionsContext, Int, WebSessionsContext), NoError> in - let activeSessionsContext = ActiveSessionsContext(account: context.account) - let webSessionsContext = WebSessionsContext(account: context.account) - let otherSessionCount = activeSessionsContext.state - |> map { state -> Int in - return state.sessions.filter({ !$0.isCurrent }).count - } - |> distinctUntilChanged - return otherSessionCount - |> map { value in - return (activeSessionsContext, value, webSessionsContext) - } - } - let activeSessionsContextAndCount = Promise<(ActiveSessionsContext, Int, WebSessionsContext)>() - activeSessionsContextAndCount.set(activeSessionsContextAndCountSignal) - - let blockedPeers = Promise(nil) - let hasTwoStepAuthPromise = Promise(nil) - - let completedProfilePhotoImpl: (UIImage) -> Void = { image in - if let data = image.jpegData(compressionQuality: 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: []) - updateState { state in - var state = state - state.updatingAvatar = .image(representation, true) - return state - } - updateAvatarDisposable.set((updateAccountPhoto(account: context.account, resource: resource, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) - }) |> deliverOnMainQueue).start(next: { result in - switch result { - case .complete: - updateState { state in - var state = state - state.updatingAvatar = nil - return state - } - case .progress: - break - } - })) - } - } - - let completedProfileVideoImpl: (UIImage, URL, TGVideoEditAdjustments?) -> Void = { image, url, adjustments in - if let data = image.jpegData(compressionQuality: 0.6) { - let photoResource = LocalFileMediaResource(fileId: arc4random64()) - context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: []) - updateState { state in - var state = state - state.updatingAvatar = .image(representation, true) - return state - } - - var videoStartTimestamp: Double? = nil - if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { - videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue - } - - let signal = Signal { subscriber in - var filteredPath = url.path - if filteredPath.hasPrefix("file://") { - filteredPath = String(filteredPath[filteredPath.index(filteredPath.startIndex, offsetBy: "file://".count)]) - } - - let avAsset = AVURLAsset(url: URL(fileURLWithPath: filteredPath)) - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(account: context.account, adjustments: adjustments) - } else { - return nil - } - } - let uploadInterface = LegacyLiveUploadInterface(account: context.account) - let signal = TGMediaVideoConverter.convert(avAsset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! - - let signalDisposable = signal.start(next: { next in - if let result = next as? TGMediaVideoConversionResult { - if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { - context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - } - - var value = stat() - if stat(result.fileURL.path, &value) == 0 { - if let data = try? Data(contentsOf: result.fileURL) { - let resource: TelegramMediaResource - if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { - resource = LocalFileMediaResource(fileId: liveUploadData.id) - } else { - resource = LocalFileMediaResource(fileId: arc4random64()) - } - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - subscriber.putNext(resource) - } - } - subscriber.putCompletion() - } - }, error: { _ in - }, completed: nil) - - let disposable = ActionDisposable { - signalDisposable?.dispose() - } - - return ActionDisposable { - disposable.dispose() - } - } - - updateAvatarDisposable.set((signal - |> mapToSignal { videoResource in - return updateAccountPhoto(account: context.account, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) - }) - } |> deliverOnMainQueue).start(next: { result in - switch result { - case .complete: - updateState { state in - var state = state - state.updatingAvatar = nil - return state - } - case .progress: - break - } - })) - } - } - - let arguments = SettingsItemArguments(sharedContext: context.sharedContext, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: { - var updating = false - updateState { - updating = $0.updatingAvatar != nil - return $0 - } - if updating { - return - } - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - let _ = (context.account.postbox.loadedPeerWithId(context.account.peerId) - |> take(1) - |> deliverOnMainQueue).start(next: { peer in - if peer.smallProfileImage != nil { - let galleryController = AvatarGalleryController(context: context, peer: peer, replaceRootController: { controller, ready in - - }) - galleryController.avatarPhotoEditCompletion = { image in - completedProfilePhotoImpl(image) - } - galleryController.avatarVideoEditCompletion = { image, url, adjustments in - completedProfileVideoImpl(image, url, adjustments) - } - hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in - avatarAndNameInfoContext.hiddenAvatarRepresentation = entry?.representations.last?.representation - updateHiddenAvatarImpl?() - })) - presentControllerImpl?(galleryController, AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in - return avatarGalleryTransitionArguments?(entry) - })) - } else { - changeProfilePhotoImpl?() - } - }) - }) - }, changeProfilePhoto: { - changeProfilePhotoImpl?() - }, openUsername: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - pushControllerImpl?(usernameSetupController(context: context)) - }) - }, openProxy: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - pushControllerImpl?(proxySettingsController(context: context)) - }) - }, openSavedMessages: { - openSavedMessagesImpl?() - }, openRecentCalls: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - pushControllerImpl?(CallListController(context: context, mode: .navigation)) - }) - }, openPrivacyAndSecurity: { privacySettingsValue in - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - let _ = (combineLatest(activeSessionsContextAndCount.get(), blockedPeers.get(), hasTwoStepAuthPromise.get()) - |> deliverOnMainQueue - |> take(1)).start(next: { sessions, blockedPeersContext, hasTwoStepAuth in - let (activeSessionsContext, _, webSessionsContext) = sessions - pushControllerImpl?(privacyAndSecurityController(context: context, initialSettings: privacySettingsValue, updatedSettings: { settings in - privacySettings.set(.single(settings)) - }, updatedBlockedPeers: { blockedPeersContext in - blockedPeers.set(.single(blockedPeersContext)) - }, updatedHasTwoStepAuth: { hasTwoStepAuthValue in - hasTwoStepAuthPromise.set(.single(hasTwoStepAuthValue)) - }, focusOnItemTag: nil, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, blockedPeersContext: blockedPeersContext, hasTwoStepAuth: hasTwoStepAuth)) - }) - }) - }, openDataAndStorage: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - pushControllerImpl?(dataAndStorageController(context: context)) - }) - }, openStickerPacks: { archivedPacksValue in - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - pushControllerImpl?(installedStickerPacksController(context: context, mode: .general, archivedPacks: archivedPacksValue, updatedPacks: { packs in - archivedPacks.set(.single(packs)) - })) - }) - }, openNotificationsAndSounds: { exceptionsList in - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - pushControllerImpl?(notificationsAndSoundsController(context: context, exceptionsList: exceptionsList)) - }) - }, openThemes: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - pushControllerImpl?(themeSettingsController(context: context)) - }) - }, pushController: { controller in - pushControllerImpl?(controller) - }, openLanguage: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - pushControllerImpl?(LocalizationListController(context: context)) - }) - }, openPassport: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - pushControllerImpl?(SecureIdAuthController(context: context, mode: .list)) - }) - }, openWallet: { - #if ENABLE_WALLET - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - context.sharedContext.openWallet(context: context, walletContext: .generic, present: { c in - pushControllerImpl?(c) - }) - }) - #endif - }, openWatch: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - pushControllerImpl?(watchSettingsController(context: context)) - }) - }, openSupport: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - let supportPeer = Promise() - supportPeer.set(supportPeerId(account: context.account)) - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - let resolvedUrlPromise = Promise() - resolvedUrlPromise.set(resolvedUrl) - - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: { - openFaq(resolvedUrlPromise, nil) - }), 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?(context.sharedContext.makeChatController(context: context, chatLocation: .peer(peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))) - } - })) - })]), nil) - }) - }, openFaq: { anchor in - let resolvedUrlPromise = Promise() - resolvedUrlPromise.set(resolvedUrl) - openFaq(resolvedUrlPromise, anchor) - }, openEditing: { - }, displayCopyContextMenu: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - let _ = (context.account.postbox.transaction { transaction -> (Peer?) in - return transaction.getPeer(context.account.peerId) - } - |> deliverOnMainQueue).start(next: { peer in - if let peer = peer { - displayCopyContextMenuImpl?(peer) - } - }) - }) - }, switchToAccount: { id in - switchToAccountImpl?(id) - }, addAccount: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - let isTestingEnvironment = context.account.testingEnvironment - let _ = accountManager.transaction({ transaction -> Void in - let _ = transaction.createAuth([AccountEnvironmentAttribute(environment: isTestingEnvironment ? .test : .production)]) - }).start() - }) - }, setAccountIdWithRevealedOptions: { accountId, fromAccountId in - updateState { state in - var state = state - if (accountId == nil && fromAccountId == state.accountIdWithRevealedOptions) || (accountId != nil && fromAccountId == nil) { - state.accountIdWithRevealedOptions = accountId - } - return state - } - }, removeAccount: { id in - removeAccountImpl?(id) - }, keepPhone: { - displayPhoneNumberConfirmation.set(false) - }, openPhoneNumberChange: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - let _ = (context.account.postbox.transaction { transaction -> String in - return (transaction.getPeer(context.account.peerId) as? TelegramUser)?.phone ?? "" - } - |> deliverOnMainQueue).start(next: { phoneNumber in - pushControllerImpl?(ChangePhoneNumberIntroController(context: context, phoneNumber: formatPhoneNumber(phoneNumber))) - }) - }) - }, accountContextAction: { id, node, gesture in - var selectedAccount: Account? - let _ = (accountsAndPeers.get() - |> take(1) - |> deliverOnMainQueue).start(next: { accountsAndPeers in - for (account, _, _) in accountsAndPeers.1 { - if account.id == id { - selectedAccount = account - break - } - } - }) - var sharedContext: SharedAccountContext? - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - sharedContext = context.sharedContext - }) - if let selectedAccount = selectedAccount, let sharedContext = sharedContext { - let accountContext = sharedContext.makeTempAccountContext(account: selectedAccount) - let chatListController = accountContext.sharedContext.makeChatListController(context: accountContext, groupId: .root, controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: enableDebugActions) - - let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 } - - let contextController = ContextController(account: accountContext.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node)), items: accountContextMenuItems(context: accountContext, logout: { - removeAccountImpl?(id) - }), reactionItems: [], gesture: gesture) - presentInGlobalOverlayImpl?(contextController, nil) - } else { - gesture?.cancel() - } - }, openDevices: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - let _ = (combineLatest(queue: .mainQueue(), - activeSessionsContextAndCount.get(), - enableQRLogin.get() - ) - |> take(1)).start(next: { activeSessionsContextAndCount, enableQRLogin in - let (activeSessionsContext, count, webSessionsContext) = activeSessionsContextAndCount - if count == 0 && enableQRLogin { - pushControllerImpl?(AuthDataTransferSplashScreen(context: context, activeSessionsContext: activeSessionsContext)) - } else { - pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false)) - } - }) - }) - }, openFilters: { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - let controller = chatListFilterPresetListController(context: context, mode: .default) - pushControllerImpl?(controller) - }) - }) - - changeProfilePhotoImpl = { - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - let _ = (context.account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in - return (transaction.getPeer(context.account.peerId), currentSearchBotsConfiguration(transaction: transaction)) - } - |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in - let presentationData = context.sharedContext.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 mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: false, personalPhoto: true, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! - let _ = currentAvatarMixin.swap(mixin) - mixin.requestSearchController = { assetsController in - let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: nil, completion: { result in - assetsController?.dismiss() - completedProfilePhotoImpl(result) - })) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } - mixin.didFinishWithImage = { image in - if let image = image { - completedProfilePhotoImpl(image) - } - } - mixin.didFinishWithVideo = { image, asset, adjustments in - if let image = image { -// completedProfileVideoImpl(image, url, adjustments) - } - } - mixin.didFinishWithDelete = { - let _ = currentAvatarMixin.swap(nil) - updateState { state in - var state = state - if let profileImage = peer?.smallProfileImage { - state.updatingAvatar = .image(profileImage, false) - } else { - state.updatingAvatar = ItemListAvatarAndNameInfoItemUpdatingAvatar.none - } - return state - } - updateAvatarDisposable.set((updateAccountPhoto(account: context.account, resource: nil, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) - }) |> deliverOnMainQueue).start(next: { result in - switch result { - case .complete: - updateState { state in - var state = state - state.updatingAvatar = nil - return state - } - 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 = contextValue.get() - |> mapToSignal { context -> Signal in - return context.account.viewTracker.peerView(context.account.peerId, updateData: true) - } - - archivedPacks.set( - .single(nil) - |> then( - contextValue.get() - |> mapToSignal { context -> Signal<[ArchivedStickerPackItem]?, NoError> in - archivedStickerPacks(account: context.account) - |> map(Optional.init) - } - ) - ) - #if ENABLE_WALLET - let hasWallet = contextValue.get() - |> mapToSignal { context in - return context.hasWalletAccess - } - #else - let hasWallet: Signal = .single(false) - #endif - - let hasPassport = ValuePromise(false) - let updatePassport: () -> Void = { - updatePassportDisposable.set(( - contextValue.get() - |> take(1) - |> mapToSignal { context -> Signal in - return twoStepAuthData(context.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 updateActiveSessions: () -> Void = { - let _ = (activeSessionsContextAndCount.get() - |> deliverOnMainQueue - |> take(1)).start(next: { activeSessionsContext, _, _ in - activeSessionsContext.loadMore() - }) - } - - let notificationsAuthorizationStatus = Promise(.allowed) - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - notificationsAuthorizationStatus.set( - .single(.allowed) - |> then( - contextValue.get() - |> mapToSignal { context -> Signal in - return DeviceAccess.authorizationStatus(applicationInForeground: context.sharedContext.applicationBindings.applicationInForeground, subject: .notifications) - } - ) - ) - } - - let notificationsWarningSuppressed = Promise(true) - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - notificationsWarningSuppressed.set( - .single(true) - |> then( - contextValue.get() - |> mapToSignal { context -> Signal in - return context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.permissionWarningKey(permission: .notifications)!) - |> map { noticeView -> Bool in - let timestamp = noticeView.value.flatMap({ ApplicationSpecificNotice.getTimestampValue($0) }) - if let timestamp = timestamp, timestamp > 0 { - return true - } else { - return false - } - } - } - ) - ) - } - - let notifyExceptions = Promise(NotificationExceptionsList(peers: [:], settings: [:])) - let updateNotifyExceptions: () -> Void = { - notifyExceptions.set( - contextValue.get() - |> take(1) - |> mapToSignal { context -> Signal in - return .single(NotificationExceptionsList(peers: [:], settings: [:])) - |> then( - notificationExceptionsList(postbox: context.account.postbox, network: context.account.network) - |> map(Optional.init) - ) - } - ) - } - - privacySettings.set( - .single(nil) - |> then( - contextValue.get() - |> mapToSignal { context -> Signal in - requestAccountPrivacySettings(account: context.account) - |> map(Optional.init) - } - ) - ) - - let hasWatchApp = contextValue.get() - |> mapToSignal { context -> Signal in - if let watchManager = context.watchManager { - return watchManager.watchAppInstalled - } else { - return .single(false) - } - } - - let updatedPresentationData = contextValue.get() - |> mapToSignal { context -> Signal in - return context.sharedContext.presentationData - } - - let preferences = context.sharedContext.accountManager.sharedData(keys: [ - SharedDataKeys.proxySettings, - ApplicationSpecificSharedDataKeys.inAppNotificationSettings, - ApplicationSpecificSharedDataKeys.experimentalUISettings - ]) - - let featuredStickerPacks = contextValue.get() - |> mapToSignal { context in - return context.account.viewTracker.featuredStickerPacks() - } - - let enableQRLoginSignal = contextValue.get() - |> mapToSignal { context -> Signal in - return context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) - |> map { view -> Bool in - guard let appConfiguration = view.values[PreferencesKeys.appConfiguration] as? AppConfiguration else { - return false - } - guard let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR else { - return false - } - return true - } - |> distinctUntilChanged - } - enableQRLogin.set(enableQRLoginSignal) - - let enableFiltersSignal = contextValue.get() - |> mapToSignal { context -> Signal in - return context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) - |> map { view -> Bool in - guard let appConfiguration = view.values[PreferencesKeys.appConfiguration] as? AppConfiguration else { - return false - } - let configuration = ChatListFilteringConfiguration(appConfiguration: appConfiguration) - return configuration.isEnabled - } - |> distinctUntilChanged - } - enableFilters.set(enableFiltersSignal) - - let signal = combineLatest(queue: Queue.mainQueue(), contextValue.get(), updatedPresentationData, statePromise.get(), peerView, combineLatest(queue: Queue.mainQueue(), preferences, notifyExceptions.get(), notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), privacySettings.get(), displayPhoneNumberConfirmation.get()), combineLatest(featuredStickerPacks, archivedPacks.get()), combineLatest(hasWallet, hasPassport.get(), hasWatchApp, enableQRLogin.get(), enableFilters.get()), accountsAndPeers.get(), activeSessionsContextAndCount.get()) - |> map { context, presentationData, state, view, preferencesAndExceptions, featuredAndArchived, hasWalletPassportAndWatch, accountsAndPeers, activeSessionsContextAndCount -> (ItemListControllerState, (ItemListNodeState, Any)) in - let otherSessionCount = activeSessionsContextAndCount.1 - - let proxySettings: ProxySettings = preferencesAndExceptions.0.entries[SharedDataKeys.proxySettings] as? ProxySettings ?? ProxySettings.defaultSettings - let inAppNotificationSettings: InAppNotificationSettings = preferencesAndExceptions.0.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings] as? InAppNotificationSettings ?? InAppNotificationSettings.defaultSettings - let experimentalUISettings: ExperimentalUISettings = preferencesAndExceptions.0.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings] as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings - - let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { - arguments.openEditing() - }) - - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), 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 searchItem = SettingsSearchItem(context: context, theme: presentationData.theme, placeholder: presentationData.strings.Common_Search, activated: state.isSearching, updateActivated: { value in - if !value { - setDisplayNavigationBarImpl?(true) - } - updateState { state in - var state = state - state.isSearching = value - return state - } - if value { - setDisplayNavigationBarImpl?(false) - } - }, presentController: { c, a in - dismissInputImpl?() - presentControllerImpl?(c, a) - }, pushController: { c in - pushControllerImpl?(c) - }, getNavigationController: getNavigationControllerImpl, resolvedFaqUrl: .complete(), exceptionsList: notifyExceptions.get(), archivedStickerPacks: archivedPacks.get(), privacySettings: privacySettings.get(), hasWallet: hasWallet, activeSessionsContext: activeSessionsContextAndCountSignal |> map { $0.0 } |> distinctUntilChanged(isEqual: { $0 === $1 }), webSessionsContext: activeSessionsContextAndCountSignal |> map { $0.2 } |> distinctUntilChanged(isEqual: { $0 === $1 })) - - let (hasWallet, hasPassport, hasWatchApp, enableQRLogin, enableFilters) = hasWalletPassportAndWatch - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: settingsEntries(account: context.account, presentationData: presentationData, state: state, view: view, proxySettings: proxySettings, notifyExceptions: preferencesAndExceptions.1, notificationsAuthorizationStatus: preferencesAndExceptions.2, notificationsWarningSuppressed: preferencesAndExceptions.3, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedPacks: featuredAndArchived.1, privacySettings: preferencesAndExceptions.4, hasWallet: hasWallet, hasPassport: hasPassport, hasWatchApp: hasWatchApp, accountsAndPeers: accountsAndPeers.1, inAppNotificationSettings: inAppNotificationSettings, experimentalUISettings: experimentalUISettings, displayPhoneNumberConfirmation: preferencesAndExceptions.5, otherSessionCount: otherSessionCount, enableQRLogin: enableQRLogin, enableFilters: enableFilters), style: .blocks, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)) - - return (controllerState, (listState, arguments)) - } - |> afterDisposed { - actionsDisposable.dispose() - } - - let icon: UIImage? - if useSpecialTabBarIcons() { - icon = UIImage(bundleImageName: "Chat List/Tabs/Holiday/IconSettings") - } else { - icon = UIImage(bundleImageName: "Chat List/Tabs/IconSettings") - } - - let notificationsFromAllAccounts = accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings]) - |> map { sharedData -> Bool in - let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings] as? InAppNotificationSettings ?? InAppNotificationSettings.defaultSettings - return settings.displayNotificationsFromAllAccounts - } - |> distinctUntilChanged - - let accountTabBarAvatarBadge: Signal = combineLatest(notificationsFromAllAccounts, accountsAndPeers.get()) - |> map { notificationsFromAllAccounts, primaryAndOther -> Int32 in - if !notificationsFromAllAccounts { - return 0 - } - let (primary, other) = primaryAndOther - if let _ = primary, !other.isEmpty { - return other.reduce(into: 0, { (result, next) in - result += next.2 - }) - } else { - return 0 - } - } - |> distinctUntilChanged - - let accountTabBarAvatar: Signal<(UIImage, UIImage)?, NoError> = combineLatest(accountsAndPeers.get(), updatedPresentationData) - |> map { primaryAndOther, presentationData -> (Account, Peer, PresentationTheme)? in - if let primary = primaryAndOther.0, !primaryAndOther.1.isEmpty { - return (primary.0, primary.1, presentationData.theme) - } else { - return nil - } - } - |> distinctUntilChanged(isEqual: { $0?.0 === $1?.0 && arePeersEqual($0?.1, $1?.1) && $0?.2 === $1?.2 }) - |> mapToSignal { primary -> Signal<(UIImage, UIImage)?, NoError> in - if let primary = primary { - let size = CGSize(width: 31.0, height: 31.0) - let inset: CGFloat = 3.0 - if let signal = peerAvatarImage(account: primary.0, peerReference: PeerReference(primary.1), authorOfMessage: nil, representation: primary.1.profileImageRepresentations.first, displayDimensions: size, inset: 3.0, emptyColor: nil, synchronousLoad: false) { - return signal - |> map { imageVersions -> (UIImage, UIImage)? in - let image = imageVersions?.0 - if let image = image, let selectedImage = generateImage(size, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.draw(image.cgImage!, in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - context.setLineWidth(1.0) - context.setStrokeColor(primary.2.rootController.tabBar.selectedIconColor.cgColor) - context.strokeEllipse(in: CGRect(x: 1.5, y: 1.5, width: 28.0, height: 28.0)) - }) { - return (image.withRenderingMode(.alwaysOriginal), selectedImage.withRenderingMode(.alwaysOriginal)) - } else { - return nil - } - } - } else { - return Signal { subscriber in - var displayLetters = primary.1.displayLetters - if displayLetters.count == 2 && displayLetters[0].isSingleEmoji && displayLetters[1].isSingleEmoji { - displayLetters = [displayLetters[0]] - } - let image = generateImage(size, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.translateBy(x: inset, y: inset) - - drawPeerAvatarLetters(context: context, size: CGSize(width: size.width - inset * 2.0, height: size.height - inset * 2.0), font: avatarFont, letters: displayLetters, peerId: primary.1.id) - })?.withRenderingMode(.alwaysOriginal) - - let selectedImage = generateImage(size, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.translateBy(x: inset, y: inset) - drawPeerAvatarLetters(context: context, size: CGSize(width: size.width - inset * 2.0, height: size.height - inset * 2.0), font: avatarFont, letters: displayLetters, peerId: primary.1.id) - context.translateBy(x: -inset, y: -inset) - context.setLineWidth(1.0) - context.setStrokeColor(primary.2.rootController.tabBar.selectedIconColor.cgColor) - context.strokeEllipse(in: CGRect(x: 1.0, y: 1.0, width: 27.0, height: 27.0)) - })?.withRenderingMode(.alwaysOriginal) - - subscriber.putNext(image.flatMap { ($0, $0) }) - subscriber.putCompletion() - return EmptyDisposable - } - |> runOn(.concurrentDefaultQueue()) - } - } else { - return .single(nil) - } - } - |> distinctUntilChanged(isEqual: { lhs, rhs in - if let lhs = lhs, let rhs = rhs { - if lhs.0 !== rhs.0 || lhs.1 !== rhs.1 { - return false - } else { - return true - } - } else if (lhs == nil) != (rhs == nil) { - return false - } - return true - }) - - let tabBarItem: Signal = combineLatest(queue: .mainQueue(), updatedPresentationData, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), accountTabBarAvatar, accountTabBarAvatarBadge) - |> map { presentationData, notificationsAuthorizationStatus, notificationsWarningSuppressed, accountTabBarAvatar, accountTabBarAvatarBadge -> ItemListControllerTabBarItem in - let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed) - var otherAccountsBadge: String? - if accountTabBarAvatarBadge > 0 { - otherAccountsBadge = compactNumericCountString(Int(accountTabBarAvatarBadge), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) - } - return ItemListControllerTabBarItem(title: presentationData.strings.Settings_Title, image: accountTabBarAvatar?.0 ?? icon, selectedImage: accountTabBarAvatar?.1 ?? icon, tintImages: accountTabBarAvatar == nil, badgeValue: notificationsWarning ? "!" : otherAccountsBadge) - } - - let controller = SettingsControllerImpl(currentContext: context, contextValue: contextValue, state: signal, tabBarItem: tabBarItem, accountsAndPeers: accountsAndPeers.get()) - pushControllerImpl = { [weak controller] value in - (controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true, animationOptions: [.removeOnMasterDetails]) - } - presentControllerImpl = { [weak controller] value, arguments in - controller?.present(value, in: .window(.root), with: arguments, blockInteraction: true) - } - presentInGlobalOverlayImpl = { [weak controller] value, arguments in - controller?.presentInGlobalOverlay(value, with: arguments) - } - dismissInputImpl = { [weak controller] in - controller?.view.window?.endEditing(true) - } - getNavigationControllerImpl = { [weak controller] in - return (controller?.navigationController as? NavigationController) - } - avatarGalleryTransitionArguments = { [weak controller] entry in - if let controller = controller { - var result: ((ASDisplayNode, CGRect, () -> (UIView?, 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 _ = (contextValue.get() - |> take(1) - |> deliverOnMainQueue).start(next: { context in - if let controller = controller, let navigationController = controller.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(context.account.peerId))) - } - }) - } - controller.tabBarItemDebugTapAction = { - let _ = (contextValue.get() - |> take(1) - |> deliverOnMainQueue).start(next: { accountContext in - pushControllerImpl?(debugController(sharedContext: accountContext.sharedContext, context: accountContext)) - }) - } - - displayCopyContextMenuImpl = { [weak controller] peer in - let _ = (contextValue.get() - |> take(1) - |> deliverOnMainQueue).start(next: { context in - if let strongController = controller { - let presentationData = context.sharedContext.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(title: presentationData.strings.Settings_CopyPhoneNumber, accessibilityLabel: presentationData.strings.Settings_CopyPhoneNumber), action: { - UIPasteboard.general.string = formatPhoneNumber(phone) - })) - } - - if let username = user.username, !username.isEmpty { - actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Settings_CopyUsername, accessibilityLabel: 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 - } - })) - } - } - }) - } - removeAccountImpl = { id in - let _ = (contextValue.get() - |> deliverOnMainQueue - |> take(1)).start(next: { context in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: presentationData.strings.Settings_LogoutConfirmationText.trimmingCharacters(in: .whitespacesAndNewlines))) - items.append(ActionSheetButtonItem(title: presentationData.strings.Settings_Logout, color: .destructive, action: { - dismissAction() - let _ = logoutFromAccount(id: id, accountManager: context.sharedContext.accountManager, alreadyLoggedOutRemotely: false).start() - })) - controller.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }) - } - switchToAccountImpl = { id in - let _ = (contextValue.get() - |> take(1) - |> deliverOnMainQueue).start(next: { context in - accountsAndPeers.set(.never()) - context.sharedContext.switchToAccount(id: id, fromSettingsController: nil, withChatListController: nil) - }) - } - controller.didAppear = { _ in - updatePassport() - updateNotifyExceptions() - updateActiveSessions() - } - controller.switchToAccount = { id in - let _ = (contextValue.get() - |> take(1) - |> deliverOnMainQueue).start(next: { context in - context.sharedContext.switchToAccount(id: id, fromSettingsController: nil, withChatListController: nil) - }) - } - controller.addAccount = { - let _ = (contextValue.get() - |> take(1) - |> deliverOnMainQueue).start(next: { context in - context.sharedContext.beginNewAuth(testingEnvironment: false) - }) - } - - controller.contentOffsetChanged = { [weak controller] offset, inVoiceOver in - if let controller = controller, let navigationBar = controller.navigationBar, let searchContentNode = navigationBar.contentNode as? NavigationBarSearchContentNode { - var offset = offset - if inVoiceOver { - offset = .known(0.0) - } - searchContentNode.updateListVisibleContentOffset(offset) - } - } - - controller.contentScrollingEnded = { [weak controller] listNode in - if let controller = controller, let navigationBar = controller.navigationBar, let searchContentNode = navigationBar.contentNode as? NavigationBarSearchContentNode { - return fixNavigationSearchableListNodeScrolling(listNode, searchNode: searchContentNode) - } - return false - } - - controller.willScrollToTop = { [weak controller] in - if let controller = controller, let navigationBar = controller.navigationBar, let searchContentNode = navigationBar.contentNode as? NavigationBarSearchContentNode { - searchContentNode.updateExpansionProgress(1.0, animated: true) - } - } - - controller.didDisappear = { [weak controller] _ in - controller?.clearItemNodesHighlight(animated: true) - setDisplayNavigationBarImpl?(true) - updateState { state in - var state = state - state.isSearching = false - return state - } - } - - setDisplayNavigationBarImpl = { [weak controller] display in - controller?.setDisplayNavigationBar(display, transition: .animated(duration: 0.5, curve: .spring)) - } - return controller -} - -private func accountContextMenuItems(context: AccountContext, logout: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> { - let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - return context.account.postbox.transaction { transaction -> [ContextMenuItem] in - var items: [ContextMenuItem] = [] - - if !transaction.getUnreadChatListPeerIds(groupId: .root, filterPredicate: nil).isEmpty { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAllAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in - let _ = (context.account.postbox.transaction { transaction in - markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: .root, filterPredicate: nil) - } - |> deliverOnMainQueue).start(completed: { - f(.default) - }) - }))) - } - - items.append(.action(ContextMenuActionItem(text: strings.Settings_Context_Logout, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Logout"), color: theme.contextMenu.destructiveColor) }, action: { _, f in - logout() - f(.default) - }))) - - return items - } -} - public func makePrivacyAndSecurityController(context: AccountContext) -> ViewController { return privacyAndSecurityController(context: context, focusOnItemTag: PrivacyAndSecurityEntryTag.autoArchive) } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 093531fe10..d57aac3d99 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1190,22 +1190,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputBackgroundWidthOffset = 36.0 } - if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { - let textCount = Int32(textInputNode.textView.text.count) - let counterColor: UIColor = textCount > inputTextMaxLength ? presentationInterfaceState.theme.chat.inputPanel.panelControlDestructiveColor : presentationInterfaceState.theme.chat.inputPanel.panelControlColor - - let remainingCount = max(-999, inputTextMaxLength - textCount) - let counterText = remainingCount >= 5 ? "" : "\(remainingCount)" - self.counterTextNode.attributedText = NSAttributedString(string: counterText, font: counterFont, textColor: counterColor) - } else { - self.counterTextNode.attributedText = NSAttributedString(string: "", font: counterFont, textColor: .black) - } - - let counterSize = self.counterTextNode.updateLayout(CGSize(width: 44.0, height: 44.0)) - let actionButtonsOriginX = width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset - let counterFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: panelHeight - minimalHeight - counterSize.height + 3.0), size: CGSize(width: width - actionButtonsOriginX - rightInset, height: counterSize.height)) - transition.updateFrame(node: self.counterTextNode, frame: counterFrame) - + self.updateCounterTextNode(transition: transition) + let actionButtonsFrame = CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight), size: CGSize(width: 44.0, height: minimalHeight)) transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame) @@ -1478,20 +1464,40 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode.primaryLanguage }) self.updateTextNodeText(animated: true) - if let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { - let textCount = Int32(textInputNode.textView.text.count) - let counterColor: UIColor = textCount > inputTextMaxLength ? presentationInterfaceState.theme.chat.inputPanel.panelControlDestructiveColor : presentationInterfaceState.theme.chat.inputPanel.panelControlColor - - let remainingCount = max(-999, inputTextMaxLength - textCount) - let counterText = remainingCount >= 5 ? "" : "\(remainingCount)" - self.counterTextNode.attributedText = NSAttributedString(string: counterText, font: counterFont, textColor: counterColor) - } else { - self.counterTextNode.attributedText = NSAttributedString(string: "", font: counterFont, textColor: .black) + self.updateCounterTextNode(transition: .immediate) + } + } + + private func updateCounterTextNode(transition: ContainedViewLayoutTransition) { + if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { + let textCount = Int32(textInputNode.textView.text.count) + let counterColor: UIColor = textCount > inputTextMaxLength ? presentationInterfaceState.theme.chat.inputPanel.panelControlDestructiveColor : presentationInterfaceState.theme.chat.inputPanel.panelControlColor + + let remainingCount = max(-999, inputTextMaxLength - textCount) + let counterText = remainingCount >= 5 ? "" : "\(remainingCount)" + self.counterTextNode.attributedText = NSAttributedString(string: counterText, font: counterFont, textColor: counterColor) + } else { + self.counterTextNode.attributedText = NSAttributedString(string: "", font: counterFont, textColor: .black) + } + + if let (width, leftInset, rightInset, maxHeight, metrics, _) = self.validLayout { + var composeButtonsOffset: CGFloat = 0.0 + if self.extendedSearchLayout { + composeButtonsOffset = 44.0 } - if let (width, leftInset, rightInset, maxHeight, metrics, isSecondary) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics) + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset, maxHeight: maxHeight, metrics: metrics) + let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) + var textFieldMinHeight: CGFloat = 33.0 + if let presentationInterfaceState = self.presentationInterfaceState { + textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics) } + let minimalHeight: CGFloat = 14.0 + textFieldMinHeight + + let counterSize = self.counterTextNode.updateLayout(CGSize(width: 44.0, height: 44.0)) + let actionButtonsOriginX = width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset + let counterFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: panelHeight - minimalHeight - counterSize.height + 3.0), size: CGSize(width: width - actionButtonsOriginX - rightInset, height: counterSize.height)) + transition.updateFrame(node: self.counterTextNode, frame: counterFrame) } }